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

jreleaser / jreleaser / #475

03 Apr 2025 10:50AM UTC coverage: 40.322% (-8.9%) from 49.193%
#475

push

github

aalmiray
feat(release): Support Forgejo as releaser

Closes #1842

Closes #1843

182 of 1099 new or added lines in 45 files covered. (16.56%)

4239 existing lines in 333 files now uncovered.

20797 of 51577 relevant lines covered (40.32%)

0.4 hits per line

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

22.67
/sdks/jreleaser-java-sdk-commons/src/main/java/org/jreleaser/sdk/commons/ClientUtils.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 com.fasterxml.jackson.core.JsonProcessingException;
21
import com.fasterxml.jackson.databind.ObjectMapper;
22
import feign.Client;
23
import feign.Feign;
24
import feign.RedirectionInterceptor;
25
import feign.Request;
26
import feign.Response;
27
import feign.form.FormData;
28
import feign.form.FormEncoder;
29
import feign.jackson.JacksonDecoder;
30
import feign.jackson.JacksonEncoder;
31
import org.apache.commons.io.IOUtils;
32
import org.apache.tika.Tika;
33
import org.apache.tika.mime.MediaType;
34
import org.jreleaser.bundle.RB;
35
import org.jreleaser.logging.JReleaserLogger;
36
import org.jreleaser.model.Constants;
37
import org.jreleaser.model.JReleaserVersion;
38
import org.jreleaser.model.api.JReleaserContext;
39
import org.jreleaser.model.internal.JReleaserModelPrinter;
40
import org.jreleaser.model.spi.announce.AnnounceException;
41
import org.jreleaser.model.spi.upload.UploadException;
42
import org.jreleaser.sdk.commons.feign.FeignLogger;
43

44
import javax.net.ssl.HostnameVerifier;
45
import javax.net.ssl.SSLContext;
46
import javax.net.ssl.SSLSession;
47
import javax.net.ssl.SSLSocketFactory;
48
import javax.net.ssl.TrustManager;
49
import javax.net.ssl.X509TrustManager;
50
import java.io.IOException;
51
import java.io.OutputStream;
52
import java.io.Reader;
53
import java.net.HttpURLConnection;
54
import java.net.URI;
55
import java.net.URISyntaxException;
56
import java.net.URL;
57
import java.nio.file.Files;
58
import java.nio.file.Path;
59
import java.security.cert.X509Certificate;
60
import java.util.Map;
61
import java.util.concurrent.TimeUnit;
62

63
import static java.nio.charset.StandardCharsets.UTF_8;
64
import static java.util.Collections.emptyMap;
65
import static java.util.Objects.requireNonNull;
66
import static org.jreleaser.util.IoUtils.newInputStreamReader;
67
import static org.jreleaser.util.StringUtils.isNotBlank;
68

69
/**
70
 * @author Andres Almiray
71
 * @since 0.2.0
72
 */
73
@org.jreleaser.infra.nativeimage.annotations.NativeImage
74
public final class ClientUtils {
75
    private static final Tika TIKA = new Tika();
1✔
76

77
    private ClientUtils() {
78
        // noop
79
    }
80

81
    public static FormData toFormData(String fileName, String contentType, String content) {
82
        return toFormData(fileName, contentType, content.getBytes(UTF_8));
×
83
    }
84

85
    public static FormData toFormData(String fileName, String contentType, byte[] content) {
86
        return FormData.builder()
×
87
            .fileName(fileName)
×
88
            .contentType(contentType)
×
89
            .data(content)
×
90
            .build();
×
91
    }
92

93
    public static FormData toFormData(Path asset) throws IOException {
94
        return toFormData(asset.getFileName().toString(),
×
95
            MediaType.parse(TIKA.detect(asset)).toString(),
×
96
            Files.readAllBytes(asset));
×
97
    }
98

99
    public static Feign.Builder builder(JReleaserContext context,
100
                                        int connectTimeout,
101
                                        int readTimeout) {
102
        requireNonNull(context, "'logger' must not be null");
1✔
103

104
        Feign.Builder builder = Feign.builder();
1✔
105

106
        if (Boolean.getBoolean("jreleaser.disableSslValidation")) {
1✔
107
            context.getLogger().warn(RB.$("warn_ssl_disabled"));
×
108
            builder = builder.client(
×
109
                new Client.Default(nonValidatingSSLSocketFactory(),
×
110
                    new NonValidatingHostnameVerifier()));
111
        }
112

113
        return builder
1✔
114
            .logger(new FeignLogger(context.getLogger()))
1✔
115
            .logLevel(FeignLogger.resolveLevel(context))
1✔
116
            .encoder(new FormEncoder(new JacksonEncoder()))
1✔
117
            .decoder(new JacksonDecoder())
1✔
118
            .responseInterceptor(new RedirectionInterceptor())
1✔
119
            .requestInterceptor(template -> template.header("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion()))
1✔
120
            .errorDecoder((methodKey, response) -> new RestAPIException(response.request(), response.status(), response.reason(), toString(context.getLogger(), response.body()), response.headers()))
1✔
121
            .options(new Request.Options(connectTimeout, TimeUnit.SECONDS, readTimeout, TimeUnit.SECONDS, true));
1✔
122
    }
123

124
    private static String toString(JReleaserLogger logger, Response.Body body) {
125
        if (null == body) return "";
1✔
126

127
        try (Reader reader = body.asReader(UTF_8)) {
1✔
128
            return IOUtils.toString(reader);
1✔
129
        } catch (IOException e) {
×
130
            logger.trace(e);
×
131
            return "";
×
132
        }
133
    }
134

135
    public static void webhook(JReleaserLogger logger,
136
                               String webhookUrl,
137
                               int connectTimeout,
138
                               int readTimeout,
139
                               Object message) throws AnnounceException {
140
        if (message instanceof String) {
×
141
            webhook(logger, webhookUrl, connectTimeout, readTimeout, (String) message);
×
142
        }
143

144
        try {
145
            ObjectMapper objectMapper = new ObjectMapper();
×
146
            webhook(logger, webhookUrl, connectTimeout, readTimeout, objectMapper.writeValueAsString(message));
×
147
        } catch (JsonProcessingException e) {
×
148
            throw new AnnounceException(e);
×
149
        }
×
150
    }
×
151

152
    public static void webhook(JReleaserLogger logger,
153
                               String webhookUrl,
154
                               int connectTimeout,
155
                               int readTimeout,
156
                               String message) throws AnnounceException {
157
        post(logger, webhookUrl, connectTimeout, readTimeout, message, emptyMap());
×
158
    }
×
159

160
    public static void post(JReleaserLogger logger,
161
                            String theUrl,
162
                            int connectTimeout,
163
                            int readTimeout,
164
                            String message,
165
                            Map<String, String> headers) throws AnnounceException {
166
        try {
167
            // create URL
168
            URL url = new URI(theUrl).toURL();
1✔
169
            // open connection
170
            logger.debug(RB.$("webhook.connection.open"));
1✔
171
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
1✔
172
            // set options
173
            logger.debug(RB.$("webhook.connection.configure"));
1✔
174
            connection.setConnectTimeout(connectTimeout * 1000);
1✔
175
            connection.setReadTimeout(readTimeout * 1000);
1✔
176
            connection.setAllowUserInteraction(false);
1✔
177
            connection.setInstanceFollowRedirects(true);
1✔
178

179
            connection.setRequestMethod("POST");
1✔
180
            connection.addRequestProperty("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion());
1✔
181
            connection.addRequestProperty("Content-Type", "application/json");
1✔
182
            connection.addRequestProperty("Accept", "application/json");
1✔
183
            headers.forEach(connection::addRequestProperty);
1✔
184
            connection.setDoOutput(true);
1✔
185

186
            // write message
187
            logger.debug(RB.$("webhook.message.send"));
1✔
188
            try (OutputStream os = connection.getOutputStream()) {
1✔
189
                byte[] input = message.getBytes(UTF_8);
1✔
190
                os.write(input, 0, input.length);
1✔
191
            }
192

193
            // handle response
194
            logger.debug(RB.$("webhook.response.handle"));
1✔
195
            int status = connection.getResponseCode();
1✔
196
            if (status >= 400) {
1✔
197
                String reason = connection.getResponseMessage();
×
198
                StringBuilder b = new StringBuilder("Webhook replied with: ")
×
199
                    .append(status);
×
200
                if (isNotBlank(reason)) {
×
201
                    b.append(" reason: ")
×
202
                        .append(reason);
×
203
                }
204
                try (Reader reader = newInputStreamReader(connection.getErrorStream())) {
×
205
                    message = IOUtils.toString(reader);
×
206
                    if (isNotBlank(message)) {
×
207
                        b.append(",")
×
208
                            .append(message);
×
209
                    }
210
                }
211
                throw new AnnounceException(b.toString());
×
212
            }
213
        } catch (URISyntaxException | IOException e) {
×
214
            logger.trace(e);
×
215
            throw new AnnounceException(e);
×
216
        }
1✔
217
    }
1✔
218

219
    public static Reader postFile(JReleaserLogger logger,
220
                                  URI uri,
221
                                  int connectTimeout,
222
                                  int readTimeout,
223
                                  FormData data,
224
                                  Map<String, String> headers) throws UploadException {
225
        headers.put("METHOD", "POST");
×
226
        return uploadFile(logger, uri, connectTimeout, readTimeout, data, headers);
×
227
    }
228

229
    public static Reader postFile(JReleaserLogger logger,
230
                                  String url,
231
                                  int connectTimeout,
232
                                  int readTimeout,
233
                                  FormData data,
234
                                  Map<String, String> headers) throws UploadException {
235
        headers.put("METHOD", "POST");
×
236
        try {
237
            return uploadFile(logger, new URI(url), connectTimeout, readTimeout, data, headers);
×
238
        } catch (URISyntaxException e) {
×
239
            logger.trace(e);
×
240
            throw new UploadException(e);
×
241
        }
242
    }
243

244
    public static Reader putFile(JReleaserLogger logger,
245
                                 String url,
246
                                 int connectTimeout,
247
                                 int readTimeout,
248
                                 FormData data,
249
                                 Map<String, String> headers) throws UploadException {
250
        headers.put("METHOD", "PUT");
×
251
        headers.put("Expect", "100-continue");
×
252
        try {
253
            return uploadFile(logger, new URI(url), connectTimeout, readTimeout, data, headers);
×
254
        } catch (URISyntaxException e) {
×
255
            logger.trace(e);
×
256
            throw new UploadException(e);
×
257
        }
258
    }
259

260
    private static Reader uploadFile(JReleaserLogger logger,
261
                                     URI uri,
262
                                     int connectTimeout,
263
                                     int readTimeout,
264
                                     FormData data,
265
                                     Map<String, String> headers) throws UploadException {
266
        try {
267
            // create URL
268
            URL theUrl = uri.toURL();
×
269
            logger.debug("url: {}", theUrl);
×
270

271
            // open connection
272
            logger.debug(RB.$("webhook.connection.open"));
×
273
            HttpURLConnection connection = (HttpURLConnection) theUrl.openConnection();
×
274
            // set options
275
            logger.debug(RB.$("webhook.connection.configure"));
×
276
            connection.setConnectTimeout(connectTimeout * 1000);
×
277
            connection.setReadTimeout(readTimeout * 1000);
×
278
            connection.setAllowUserInteraction(false);
×
279
            connection.setInstanceFollowRedirects(true);
×
280

281
            connection.setRequestMethod(headers.remove("METHOD"));
×
282
            if (!headers.containsKey("Accept")) {
×
283
                connection.addRequestProperty("Accept", "*/*");
×
284
            }
285
            connection.addRequestProperty("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion());
×
286
            connection.addRequestProperty("Content-Length", data.getData().length + "");
×
287
            connection.setRequestProperty("Content-Type", data.getContentType());
×
288
            headers.forEach(connection::setRequestProperty);
×
289

290
            connection.getRequestProperties().forEach((k, v) -> {
×
291
                if (JReleaserModelPrinter.isSecret(k)) {
×
292
                    logger.debug("{}: {}", k, Constants.HIDE);
×
293
                } else {
294
                    logger.debug("{}: {}", k, v);
×
295
                }
296
            });
×
297

298
            connection.setDoOutput(true);
×
299

300
            // write message
301
            logger.debug(RB.$("webhook.data.send"));
×
302
            try (OutputStream os = connection.getOutputStream()) {
×
303
                os.write(data.getData(), 0, data.getData().length);
×
304
                os.flush();
×
305
            }
306

307
            // handle response
308
            logger.debug(RB.$("webhook.response.handle"));
×
309
            int status = connection.getResponseCode();
×
310
            if (status >= 400) {
×
311
                String reason = connection.getResponseMessage();
×
312
                StringBuilder b = new StringBuilder("Got ")
×
313
                    .append(status);
×
314
                if (isNotBlank(reason)) {
×
315
                    b.append(" reason: ")
×
316
                        .append(reason);
×
317
                }
318
                logger.trace(RB.$("webhook.server.reply", status, reason));
×
319

320
                try (Reader reader = newInputStreamReader(connection.getErrorStream())) {
×
321
                    String message = IOUtils.toString(reader);
×
322
                    if (isNotBlank(message)) {
×
323
                        b.append(", ")
×
324
                            .append(message);
×
325
                    }
326
                }
327
                throw new UploadException(b.toString());
×
328
            }
329

330
            return newInputStreamReader(connection.getInputStream());
×
331
        } catch (IOException e) {
×
332
            logger.trace(e);
×
333
            throw new UploadException(e);
×
334
        }
335
    }
336

337
    public static boolean head(JReleaserLogger logger,
338
                               String theUrl,
339
                               int connectTimeout,
340
                               int readTimeout) throws RestAPIException {
341
        try {
342
            // create URL
UNCOV
343
            URL url = new URI(theUrl).toURL();
×
344
            // open connection
UNCOV
345
            logger.debug(RB.$("webhook.connection.open"));
×
UNCOV
346
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
×
347
            // set options
UNCOV
348
            logger.debug(RB.$("webhook.connection.configure"));
×
UNCOV
349
            connection.setConnectTimeout(connectTimeout * 1000);
×
UNCOV
350
            connection.setReadTimeout(readTimeout * 1000);
×
UNCOV
351
            connection.setAllowUserInteraction(false);
×
UNCOV
352
            connection.setInstanceFollowRedirects(true);
×
353

UNCOV
354
            connection.setRequestMethod("HEAD");
×
UNCOV
355
            connection.addRequestProperty("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion());
×
356

357
            // handle response
UNCOV
358
            logger.debug(RB.$("webhook.response.handle"));
×
UNCOV
359
            int status = connection.getResponseCode();
×
UNCOV
360
            if (status == 200) return true;
×
UNCOV
361
            if (status == 404) return false;
×
362

363
            String reason = connection.getResponseMessage();
×
364
            StringBuilder b = new StringBuilder("Request replied with: ")
×
365
                .append(status);
×
366
            if (isNotBlank(reason)) {
×
367
                b.append(" reason: ")
×
368
                    .append(reason);
×
369
            }
370
            throw new RestAPIException(status, b.toString());
×
371
        } catch (URISyntaxException | IOException e) {
×
372
            logger.trace(e);
×
373
            throw new RestAPIException(500, e);
×
374
        }
375
    }
376

377
    private static SSLSocketFactory nonValidatingSSLSocketFactory() {
378
        try {
379
            SSLContext sslContext = SSLContext.getInstance("SSL");
×
380
            sslContext.init(null, new TrustManager[]{new NonValidatingTrustManager()}, null); // lgtm [java/insecure-trustmanager]
×
381
            return sslContext.getSocketFactory();
×
382
        } catch (Exception e) {
×
383
            throw new IllegalStateException(e);
×
384
        }
385
    }
386

387
    private static class NonValidatingTrustManager implements X509TrustManager {
388
        private static final X509Certificate[] EMPTY_CERTIFICATES = new X509Certificate[0];
×
389

390
        @Override
391
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
392
            // noop
393
        }
×
394

395
        @Override
396
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
397
            // noop
398
        }
×
399

400
        @Override
401
        public X509Certificate[] getAcceptedIssuers() {
402
            return EMPTY_CERTIFICATES;
×
403
        }
404
    }
405

406
    private static class NonValidatingHostnameVerifier implements HostnameVerifier {
407
        @Override
408
        public boolean verify(String hostname, SSLSession session) {
409
            return true;
×
410
        }
411
    }
412
}
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