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

jreleaser / jreleaser / #477

04 Apr 2025 05:53PM UTC coverage: 35.124% (-5.1%) from 40.183%
#477

push

github

aalmiray
fix(deploy): Add missing Forgejo messages

Related to #1842

18210 of 51845 relevant lines covered (35.12%)

0.35 hits per line

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

0.0
/sdks/jreleaser-forgejo-java-sdk/src/main/java/org/jreleaser/sdk/forgejo/Forgejo.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.forgejo;
19

20
import com.fasterxml.jackson.annotation.JsonInclude;
21
import com.fasterxml.jackson.databind.DeserializationFeature;
22
import com.fasterxml.jackson.databind.ObjectMapper;
23
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
24
import com.fasterxml.jackson.databind.SerializationFeature;
25
import feign.form.FormData;
26
import feign.form.FormEncoder;
27
import feign.httpclient.ApacheHttpClient;
28
import feign.jackson.JacksonDecoder;
29
import feign.jackson.JacksonEncoder;
30
import org.apache.tika.Tika;
31
import org.apache.tika.mime.MediaType;
32
import org.jreleaser.bundle.RB;
33
import org.jreleaser.model.api.JReleaserContext;
34
import org.jreleaser.sdk.commons.ClientUtils;
35
import org.jreleaser.sdk.commons.RestAPIException;
36
import org.jreleaser.sdk.forgejo.api.Asset;
37
import org.jreleaser.sdk.forgejo.api.Branch;
38
import org.jreleaser.sdk.forgejo.api.ForgejoAPI;
39
import org.jreleaser.sdk.forgejo.api.Issue;
40
import org.jreleaser.sdk.forgejo.api.Label;
41
import org.jreleaser.sdk.forgejo.api.Milestone;
42
import org.jreleaser.sdk.forgejo.api.Organization;
43
import org.jreleaser.sdk.forgejo.api.Release;
44
import org.jreleaser.sdk.forgejo.api.Repository;
45
import org.jreleaser.sdk.forgejo.api.SearchUser;
46
import org.jreleaser.sdk.forgejo.api.User;
47
import org.jreleaser.sdk.forgejo.internal.Page;
48
import org.jreleaser.sdk.forgejo.internal.PaginatingDecoder;
49
import org.jreleaser.util.CollectionUtils;
50

51
import java.io.IOException;
52
import java.nio.file.Files;
53
import java.nio.file.Path;
54
import java.util.ArrayList;
55
import java.util.LinkedHashMap;
56
import java.util.List;
57
import java.util.Map;
58
import java.util.Optional;
59
import java.util.Set;
60

61
import static java.util.Objects.requireNonNull;
62
import static java.util.stream.Collectors.toList;
63
import static org.jreleaser.util.StringUtils.requireNonBlank;
64

65
/**
66
 * @author Andres Almiray
67
 * @since 1.18.0
68
 */
69
public class Forgejo {
70
    private static final String API_V1 = "/api/v1";
71
    private final Tika tika = new Tika();
×
72

73
    private final JReleaserContext context;
74
    private final ForgejoAPI api;
75

76
    public Forgejo(JReleaserContext context,
77
                 String endpoint,
78
                 String token,
79
                 int connectTimeout,
80
                 int readTimeout) {
×
81
        requireNonNull(context, "'context' must not be null");
×
82
        requireNonBlank(token, "'token' must not be blank");
×
83
        requireNonBlank(endpoint, "'endpoint' must not be blank");
×
84

85
        if (!endpoint.endsWith(API_V1)) {
×
86
            if (endpoint.endsWith("/")) {
×
87
                endpoint = endpoint.substring(0, endpoint.length() - 1);
×
88
            }
89
            endpoint += API_V1;
×
90
        }
91

92
        ObjectMapper objectMapper = new ObjectMapper()
×
93
            .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
×
94
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
×
95
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
×
96
            .configure(SerializationFeature.INDENT_OUTPUT, true);
×
97

98
        this.context = context;
×
99
        this.api = ClientUtils.builder(context, connectTimeout, readTimeout)
×
100
            .client(new ApacheHttpClient())
×
101
            .encoder(new FormEncoder(new JacksonEncoder(objectMapper)))
×
102
            .decoder(new PaginatingDecoder(new JacksonDecoder(objectMapper)))
×
103
            .requestInterceptor(template -> template.header("Authorization", String.format("token %s", token)))
×
104
            .target(ForgejoAPI.class, endpoint);
×
105
    }
×
106

107
    public Repository findRepository(String owner, String repo) {
108
        context.getLogger().debug(RB.$("git.repository.lookup"), owner, repo);
×
109
        try {
110
            return api.getRepository(owner, repo);
×
111
        } catch (RestAPIException e) {
×
112
            if (e.isNotFound()) {
×
113
                // ok
114
                return null;
×
115
            }
116
            throw e;
×
117
        }
118
    }
119

120
    public List<org.jreleaser.model.spi.release.Release> listReleases(String owner, String repoName) {
121
        context.getLogger().debug(RB.$("git.list.releases"), owner, repoName);
×
122

123
        List<org.jreleaser.model.spi.release.Release> releases = new ArrayList<>();
×
124

125
        int pageCount = 0;
×
126
        Map<String, Object> params = CollectionUtils.<String, Object>map()
×
127
            .e("draft", false)
×
128
            .e("prerelease", false)
×
129
            .e("limit", 20);
×
130

131
        boolean consume = true;
×
132
        do {
133
            params.put("page", ++pageCount);
×
134
            Page<List<Release>> page = api.listReleases(owner, repoName, params);
×
135
            page.getContent().stream()
×
136
                .map(r -> new org.jreleaser.model.spi.release.Release(
×
137
                    r.getName(),
×
138
                    r.getTagName(),
×
139
                    r.getHtmlUrl(),
×
140
                    r.getPublishedAt()
×
141
                ))
142
                .forEach(releases::add);
×
143

144
            if (!page.hasLinks() || !page.getLinks().hasNext()) {
×
145
                consume = false;
×
146
            }
147
        }
148
        while (consume);
×
149

150
        return releases;
×
151
    }
152

153
    public List<String> listBranches(String owner, String repoName) {
154
        context.getLogger().debug(RB.$("git.list.branches"), owner, repoName);
×
155

156
        List<String> branches = new ArrayList<>();
×
157

158
        int pageCount = 0;
×
159
        Map<String, Object> params = CollectionUtils.<String, Object>map()
×
160
            .e("limit", 20);
×
161

162
        boolean consume = true;
×
163
        do {
164
            params.put("page", ++pageCount);
×
165
            Page<List<Branch>> page = api.listBranches(owner, repoName, params);
×
166
            page.getContent().stream()
×
167
                .map(Branch::getName)
×
168
                .forEach(branches::add);
×
169

170
            if (!page.hasLinks() || !page.getLinks().hasNext()) {
×
171
                consume = false;
×
172
            }
173
        }
174
        while (consume);
×
175

176
        return branches;
×
177
    }
178

179
    public Map<String, Asset> listAssets(String owner, String repo, Release release) {
180
        context.getLogger().debug(RB.$("git.list.assets.github"), owner, repo, release.getId());
×
181

182
        Map<String, Asset> assets = new LinkedHashMap<>();
×
183
        for (Asset asset : api.listAssets(owner, repo, release.getId())) {
×
184
            assets.put(asset.getName(), asset);
×
185
        }
×
186

187
        return assets;
×
188
    }
189

190
    public Optional<Milestone> findMilestoneByName(String owner, String repo, String milestoneName) {
191
        context.getLogger().debug(RB.$("git.milestone.lookup"), milestoneName, owner, repo);
×
192

193
        return findMilestone(owner, repo, milestoneName, "open");
×
194
    }
195

196
    public Optional<Milestone> findClosedMilestoneByName(String owner, String repo, String milestoneName) {
197
        context.getLogger().debug(RB.$("git.milestone.lookup.closed"), milestoneName, owner, repo);
×
198

199
        return findMilestone(owner, repo, milestoneName, "closed");
×
200
    }
201

202
    private Optional<Milestone> findMilestone(String owner, String repo, String milestoneName, String state) {
203
        try {
204
            Milestone milestone = api.findMilestoneByTitle(owner, repo, milestoneName);
×
205

206
            if (null == milestone) {
×
207
                return Optional.empty();
×
208
            }
209

210
            return state.equals(milestone.getState()) ? Optional.of(milestone) : Optional.empty();
×
211
        } catch (RestAPIException e) {
×
212
            if (e.isNotFound()) {
×
213
                // ok
214
                return Optional.empty();
×
215
            }
216
            throw e;
×
217
        }
218
    }
219

220
    public void closeMilestone(String owner, String repo, Milestone milestone) {
221
        context.getLogger().debug(RB.$("git.milestone.close"), milestone.getTitle(), owner, repo);
×
222

223
        api.updateMilestone(CollectionUtils.<String, Object>map()
×
224
            .e("state", "closed"), owner, repo, milestone.getId());
×
225
    }
×
226

227
    public Repository createRepository(String owner, String repo) {
228
        context.getLogger().debug(RB.$("git.repository.create"), owner, repo);
×
229

230
        Map<String, Object> params = CollectionUtils.<String, Object>map()
×
231
            .e("name", repo)
×
232
            .e("private", false);
×
233

234
        Organization organization = resolveOrganization(owner);
×
235
        if (null != organization) {
×
236
            return api.createRepository(params, owner);
×
237
        }
238

239
        return api.createRepository(params);
×
240
    }
241

242
    private Organization resolveOrganization(String name) {
243
        try {
244
            return api.getOrganization(name);
×
245
        } catch (RestAPIException e) {
×
246
            if (e.isNotFound()) {
×
247
                // ok
248
                return null;
×
249
            }
250
            throw e;
×
251
        }
252
    }
253

254
    public Release findReleaseByTag(String owner, String repo, String tagName) {
255
        context.getLogger().debug(RB.$("git.fetch.release.by.tag"), owner, repo, tagName);
×
256

257
        try {
258
            return api.getReleaseByTagName(owner, repo, tagName);
×
259
        } catch (RestAPIException e) {
×
260
            if (e.isNotFound()) {
×
261
                // ok
262
                return null;
×
263
            }
264
            throw e;
×
265
        }
266
    }
267

268
    public void deleteRelease(String owner, String repo, String tagName, Integer id) throws RestAPIException {
269
        context.getLogger().debug(RB.$("git.delete.release.from.id"), tagName, owner, repo, id);
×
270

271
        try {
272
            api.deleteRelease(owner, repo, id);
×
273
        } catch (RestAPIException e) {
×
274
            if (e.isNotFound()) {
×
275
                // OK. Release might have been deleted but
276
                // tag still exists.
277
                return;
×
278
            }
279
            throw e;
×
280
        }
×
281
    }
×
282

283
    public void deleteTag(String owner, String repo, String tagName) throws RestAPIException {
284
        context.getLogger().debug(RB.$("git.delete.tag.from"), tagName, owner, repo);
×
285

286
        api.deleteTag(owner, repo, tagName);
×
287
    }
×
288

289
    public void deletePackage(String owner, String type, String name, String version) throws RestAPIException {
290
        context.getLogger().debug(RB.$("forgejo.delete.package"), owner, type, name, version);
×
291

292
        api.deletePackage(owner, type, name, version);
×
293
    }
×
294

295
    public Release createRelease(String owner, String repo, Release release) throws RestAPIException {
296
        context.getLogger().debug(RB.$("git.create.release"), owner, repo, release.getTagName());
×
297

298
        return api.createRelease(release, owner, repo);
×
299
    }
300

301
    public void updateRelease(String owner, String repo, Integer id, Release release) throws RestAPIException {
302
        context.getLogger().debug(RB.$("git.update.release"), owner, repo, release.getTagName());
×
303

304
        api.updateRelease(release, owner, repo, id);
×
305
    }
×
306

307
    public void uploadAssets(String owner, String repo, Release release, Set<org.jreleaser.model.spi.release.Asset> assets) throws IOException {
308
        for (org.jreleaser.model.spi.release.Asset asset : assets) {
×
309
            if (0 == Files.size(asset.getPath()) || !Files.exists(asset.getPath())) {
×
310
                // do not upload empty or non existent files
311
                continue;
×
312
            }
313

314
            uploadOrUpdateAsset(asset, owner, repo, release,
×
315
                "git.upload.asset", "git.upload.asset.failure");
316
        }
×
317
    }
×
318

319
    public void updateAssets(String owner, String repo, Release release, Set<org.jreleaser.model.spi.release.Asset> assets, Map<String, Asset> existingAssets) throws IOException {
320
        for (org.jreleaser.model.spi.release.Asset asset : assets) {
×
321
            if (0 == Files.size(asset.getPath()) || !Files.exists(asset.getPath())) {
×
322
                // do not upload empty or non existent files
323
                continue;
×
324
            }
325

326
            context.getLogger().debug(" " + RB.$("git.delete.asset"), asset.getFilename());
×
327
            try {
328
                api.deleteAsset(owner, repo, release.getId(), existingAssets.get(asset.getFilename()).getId());
×
329
            } catch (RestAPIException e) {
×
330
                context.getLogger().error(" " + RB.$("git.delete.asset.failure"), asset.getFilename());
×
331
                throw e;
×
332
            }
×
333

334
            uploadOrUpdateAsset(asset, owner, repo, release,
×
335
                "git.update.asset", "git.update.asset.failure");
336
        }
×
337
    }
×
338

339
    private void uploadOrUpdateAsset(org.jreleaser.model.spi.release.Asset asset, String owner, String repo, Release release, String operationMessageKey, String operationErrorMessageKey) throws IOException {
340
        context.getLogger().info(" " + RB.$(operationMessageKey), asset.getFilename());
×
341
        try {
342
            api.uploadAsset(owner, repo, release.getId(), toFormData(asset.getPath()));
×
343
        } catch (RestAPIException e) {
×
344
            context.getLogger().error(" " + RB.$(operationErrorMessageKey), asset.getFilename());
×
345
            throw e;
×
346
        }
×
347
    }
×
348

349
    public Optional<org.jreleaser.model.spi.release.User> findUser(String email, String name, String host) throws RestAPIException {
350
        context.getLogger().debug(RB.$("git.user.lookup"), name, email);
×
351

352
        SearchUser search = api.searchUser(CollectionUtils.<String, String>mapOf("q", email));
×
353
        if (null != search.getData() && !search.getData().isEmpty()) {
×
354
            User user = search.getData().get(0);
×
355
            return Optional.of(new org.jreleaser.model.spi.release.User(user.getUsername(), email, host + user.getUsername()));
×
356
        }
357

358
        return Optional.empty();
×
359
    }
360

361
    public Label getOrCreateLabel(String owner, String name, String labelName, String labelColor, String description) {
362
        context.getLogger().debug(RB.$("git.label.fetch", labelName));
×
363

364
        List<Label> labels = listLabels(owner, name);
×
365
        Optional<Label> label = labels.stream()
×
366
            .filter(l -> l.getName().equals(labelName))
×
367
            .findFirst();
×
368

369
        if (label.isPresent()) {
×
370
            return label.get();
×
371
        }
372

373
        context.getLogger().debug(RB.$("git.label.create", labelName));
×
374
        return api.createLabel(owner, name, labelName, labelColor, description);
×
375
    }
376

377
    public Optional<Issue> findIssue(String owner, String name, int issueNumber) {
378
        context.getLogger().debug(RB.$("git.issue.fetch", issueNumber));
×
379
        try {
380
            return Optional.of(api.findIssue(owner, name, issueNumber));
×
381
        } catch (RestAPIException e) {
×
382
            if (e.isNotFound()) {
×
383
                return Optional.empty();
×
384
            }
385
            throw e;
×
386
        }
387
    }
388

389
    public void addLabelToIssue(String owner, String name, Issue issue, Label label) {
390
        context.getLogger().debug(RB.$("git.issue.label", label.getName(), issue.getNumber()));
×
391

392
        Map<String, List<Integer>> labels = new LinkedHashMap<>();
×
393
        List<Integer> list = labels.computeIfAbsent("labels", k -> new ArrayList<>());
×
394
        list.addAll(issue.getLabels().stream().map(Label::getId).collect(toList()));
×
395
        list.add(label.getId());
×
396

397
        api.labelIssue(labels, owner, name, issue.getNumber());
×
398
    }
×
399

400
    public void commentOnIssue(String owner, String name, Issue issue, String comment) {
401
        context.getLogger().debug(RB.$("git.issue.comment", issue.getNumber()));
×
402

403
        Map<String, String> params = new LinkedHashMap<>();
×
404
        params.put("body", comment);
×
405

406
        api.commentIssue(params, owner, name, issue.getNumber());
×
407
    }
×
408

409
    public void setMilestoneOnIssue(String owner, String name, Issue issue, Milestone milestone) {
410
        Map<String, Object> params = new LinkedHashMap<>();
×
411
        params.put("milestone", milestone.getId());
×
412

413
        api.updateIssue(params, owner, name, issue.getNumber());
×
414
    }
×
415

416
    private List<Label> listLabels(String owner, String repoName) {
417
        context.getLogger().debug(RB.$("git.list.labels"), owner, repoName);
×
418

419
        List<Label> labels = new ArrayList<>();
×
420

421
        int pageCount = 0;
×
422
        Map<String, Object> params = CollectionUtils.<String, Object>map()
×
423
            .e("limit", 20);
×
424

425
        boolean consume = true;
×
426
        do {
427
            params.put("page", ++pageCount);
×
428
            Page<List<Label>> page = api.listLabels(owner, repoName, params);
×
429
            labels.addAll(page.getContent());
×
430

431
            if (!page.hasLinks() || !page.getLinks().hasNext()) {
×
432
                consume = false;
×
433
            }
434
        }
435
        while (consume);
×
436

437
        return labels;
×
438
    }
439

440
    private FormData toFormData(Path asset) throws IOException {
441
        return FormData.builder()
×
442
            .fileName(asset.getFileName().toString())
×
443
            .contentType(MediaType.parse(tika.detect(asset)).toString())
×
444
            .data(Files.readAllBytes(asset))
×
445
            .build();
×
446
    }
447
}
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