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

jreleaser / jreleaser / #558

08 Dec 2025 02:56PM UTC coverage: 48.239% (+0.02%) from 48.215%
#558

push

github

aalmiray
feat(core): warn when a name template cannot be resolved

Closes #1960

Closes #1961

299 of 573 new or added lines in 133 files covered. (52.18%)

4 existing lines in 4 files now uncovered.

26047 of 53996 relevant lines covered (48.24%)

0.48 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/ForgejoReleaser.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 org.jreleaser.bundle.RB;
21
import org.jreleaser.model.UpdateSection;
22
import org.jreleaser.model.api.common.Apply;
23
import org.jreleaser.model.api.common.ExtraProperties;
24
import org.jreleaser.model.internal.JReleaserContext;
25
import org.jreleaser.model.internal.util.VersionUtils;
26
import org.jreleaser.model.spi.release.ReleaseException;
27
import org.jreleaser.model.spi.release.User;
28
import org.jreleaser.mustache.TemplateContext;
29
import org.jreleaser.sdk.commons.RestAPIException;
30
import org.jreleaser.sdk.forgejo.api.Asset;
31
import org.jreleaser.sdk.forgejo.api.Issue;
32
import org.jreleaser.sdk.forgejo.api.Label;
33
import org.jreleaser.sdk.forgejo.api.Milestone;
34
import org.jreleaser.sdk.forgejo.api.Release;
35
import org.jreleaser.sdk.forgejo.api.Repository;
36
import org.jreleaser.sdk.git.ChangelogProvider;
37
import org.jreleaser.sdk.git.GitSdk;
38
import org.jreleaser.sdk.git.release.AbstractReleaser;
39

40
import java.io.IOException;
41
import java.nio.file.Files;
42
import java.util.LinkedHashMap;
43
import java.util.List;
44
import java.util.Map;
45
import java.util.Optional;
46
import java.util.Set;
47
import java.util.TreeSet;
48
import java.util.regex.Pattern;
49

50
import static org.jreleaser.mustache.Templates.resolveTemplate;
51
import static org.jreleaser.util.StringUtils.capitalize;
52
import static org.jreleaser.util.StringUtils.uncapitalize;
53

54
/**
55
 * @author Andres Almiray
56
 * @since 1.18.0
57
 */
58
@org.jreleaser.infra.nativeimage.annotations.NativeImage
59
public class ForgejoReleaser extends AbstractReleaser<org.jreleaser.model.api.release.ForgejoReleaser> {
60
    private static final long serialVersionUID = -8160459963248847787L;
61

62
    private final org.jreleaser.model.internal.release.ForgejoReleaser forgejo;
63

64
    public ForgejoReleaser(JReleaserContext context, Set<org.jreleaser.model.spi.release.Asset> assets) {
65
        super(context, assets);
×
66
        forgejo = context.getModel().getRelease().getForgejo();
×
67
    }
×
68

69
    @Override
70
    public org.jreleaser.model.api.release.ForgejoReleaser getReleaser() {
71
        return forgejo.asImmutable();
×
72
    }
73

74
    @Override
75
    protected void createRelease() throws ReleaseException {
76
        String pullBranch = forgejo.getBranch();
×
NEW
77
        String pushBranch = forgejo.getResolvedBranchPush(context);
×
78
        boolean mustCheckoutBranch = !pushBranch.equals(pullBranch);
×
NEW
79
        context.getLogger().info(RB.$("git.releaser.releasing"), forgejo.getResolvedRepoUrl(context), pushBranch);
×
NEW
80
        String tagName = forgejo.getEffectiveTagName(context);
×
81

82
        try {
83
            Forgejo api = new Forgejo(context.asImmutable(),
×
84
                forgejo.getApiEndpoint(),
×
85
                forgejo.getToken(),
×
86
                forgejo.getConnectTimeout(),
×
87
                forgejo.getReadTimeout());
×
88

89
            if (!context.isDryrun()) {
×
90
                List<String> branchNames = api.listBranches(forgejo.getOwner(), forgejo.getName());
×
91
                GitSdk.of(context).checkoutBranch(forgejo, pushBranch, mustCheckoutBranch, !branchNames.contains(pushBranch));
×
92
            }
93

94
            String changelog = context.getChangelog().getResolvedChangelog();
×
95

96
            context.getLogger().debug(RB.$("git.releaser.release.lookup"), tagName, forgejo.getCanonicalRepoName());
×
97
            Release release = findReleaseByTag(api, tagName);
×
98
            boolean snapshot = context.getModel().getProject().isSnapshot();
×
99
            if (null != release) {
×
100
                context.getLogger().debug(RB.$("git.releaser.release.exists"), tagName);
×
101
                if (forgejo.isOverwrite() || snapshot) {
×
102
                    context.getLogger().debug(RB.$("git.releaser.release.delete"), tagName);
×
103
                    if (!context.isDryrun()) {
×
104
                        api.deleteRelease(forgejo.getOwner(), forgejo.getName(), tagName, release.getId());
×
105
                    }
106
                    context.getLogger().debug(RB.$("git.releaser.release.create"), tagName);
×
107
                    createRelease(api, tagName, changelog, snapshot && forgejo.isMatch());
×
108
                } else if (forgejo.getUpdate().isEnabled()) {
×
109
                    context.getLogger().debug(RB.$("git.releaser.release.update"), tagName);
×
110
                    if (!context.isDryrun()) {
×
111
                        Release updater = new Release();
×
112
                        if (forgejo.getPrerelease().isEnabledSet()) {
×
113
                            updater.setPrerelease(forgejo.getPrerelease().isEnabled());
×
114
                        }
115
                        if (forgejo.isDraftSet()) {
×
116
                            updater.setDraft(forgejo.isDraft());
×
117
                        }
118
                        if (forgejo.getUpdate().getSections().contains(UpdateSection.TITLE)) {
×
119
                            context.getLogger().info(RB.$("git.releaser.release.update.title"), forgejo.getEffectiveReleaseName());
×
120
                            updater.setName(forgejo.getEffectiveReleaseName());
×
121
                        }
122
                        if (forgejo.getUpdate().getSections().contains(UpdateSection.BODY)) {
×
123
                            context.getLogger().info(RB.$("git.releaser.release.update.body"));
×
124
                            updater.setBody(changelog);
×
125
                        }
126
                        api.updateRelease(forgejo.getOwner(), forgejo.getName(), release.getId(), updater);
×
127

128
                        if (forgejo.getUpdate().getSections().contains(UpdateSection.ASSETS)) {
×
129
                            updateAssets(api, release);
×
130
                        }
131
                        updateIssues(forgejo, api);
×
132
                    }
×
133
                } else {
134
                    if (context.isDryrun()) {
×
135
                        context.getLogger().debug(RB.$("git.releaser.release.create"), tagName);
×
136
                        createRelease(api, tagName, changelog, false);
×
137
                        return;
×
138
                    }
139

140
                    throw new IllegalStateException(RB.$("ERROR_git_releaser_cannot_release",
×
141
                        capitalize(forgejo.getServiceName()), tagName));
×
142
                }
143
            } else {
144
                context.getLogger().debug(RB.$("git.releaser.release.not.found"), tagName);
×
145
                context.getLogger().debug(RB.$("git.releaser.release.create"), tagName);
×
146
                createRelease(api, tagName, changelog, snapshot && forgejo.isMatch());
×
147
            }
148
        } catch (RestAPIException | IOException | IllegalStateException e) {
×
149
            context.getLogger().trace(e);
×
150
            throw new ReleaseException(e);
×
151
        }
×
152
    }
×
153

154
    private Release findReleaseByTag(Forgejo api, String tagName) {
155
        if (context.isDryrun()) return null;
×
156
        return api.findReleaseByTag(forgejo.getOwner(), forgejo.getName(), tagName);
×
157
    }
158

159
    protected org.jreleaser.model.spi.release.Repository.Kind resolveRepositoryKind() {
160
        return org.jreleaser.model.spi.release.Repository.Kind.OTHER;
×
161
    }
162

163
    @Override
164
    public org.jreleaser.model.spi.release.Repository maybeCreateRepository(String owner, String repo, String password, ExtraProperties extraProperties) throws IOException {
165
        context.getLogger().debug(RB.$("git.repository.lookup"), owner, repo);
×
166

167
        Forgejo api = new Forgejo(context.asImmutable(),
×
168
            forgejo.getApiEndpoint(),
×
169
            password,
170
            forgejo.getConnectTimeout(),
×
171
            forgejo.getReadTimeout());
×
172
        Repository repository = api.findRepository(owner, repo);
×
173
        if (null == repository) {
×
174
            repository = api.createRepository(owner, repo);
×
175
        }
176

177
        return new org.jreleaser.model.spi.release.Repository(
×
178
            resolveRepositoryKind(),
×
179
            owner,
180
            repo,
181
            repository.getHtmlUrl(),
×
182
            repository.getCloneUrl());
×
183
    }
184

185
    @Override
186
    public Optional<User> findUser(String email, String name) {
187
        try {
188
            String host = forgejo.getHost();
×
189
            String endpoint = forgejo.getApiEndpoint();
×
190
            if (endpoint.startsWith("https")) {
×
191
                host = "https://" + host;
×
192
            } else {
193
                host = "http://" + host;
×
194
            }
195
            if (!host.endsWith("/")) {
×
196
                host += "/";
×
197
            }
198

199
            return new Forgejo(context.asImmutable(),
×
200
                forgejo.getApiEndpoint(),
×
201
                forgejo.getToken(),
×
202
                forgejo.getConnectTimeout(),
×
203
                forgejo.getReadTimeout())
×
204
                .findUser(email, name, host);
×
205
        } catch (RestAPIException e) {
×
206
            context.getLogger().trace(e);
×
207
            context.getLogger().debug(RB.$("git.releaser.user.not.found"), email);
×
208
        }
209

210
        return Optional.empty();
×
211
    }
212

213
    @Override
214
    public List<org.jreleaser.model.spi.release.Release> listReleases(String owner, String repo) throws IOException {
215
        Forgejo api = new Forgejo(context.asImmutable(),
×
216
            forgejo.getApiEndpoint(),
×
217
            forgejo.getToken(),
×
218
            forgejo.getConnectTimeout(),
×
219
            forgejo.getReadTimeout());
×
220

221
        List<org.jreleaser.model.spi.release.Release> releases = api.listReleases(owner, repo);
×
222

223
        VersionUtils.clearUnparseableTags();
×
224
        Pattern versionPattern = VersionUtils.resolveVersionPattern(context);
×
225
        for (org.jreleaser.model.spi.release.Release release : releases) {
×
226
            release.setVersion(VersionUtils.version(context, release.getTagName(), versionPattern));
×
227
        }
×
228

229
        releases.sort((r1, r2) -> r2.getVersion().compareTo(r1.getVersion()));
×
230

231
        return releases;
×
232
    }
233

234
    private void createRelease(Forgejo api, String tagName, String changelog, boolean deleteTags) throws IOException {
235
        if (context.isDryrun()) {
×
236
            for (org.jreleaser.model.spi.release.Asset asset : assets) {
×
237
                if (0 == Files.size(asset.getPath()) || !Files.exists(asset.getPath())) {
×
238
                    // do not upload empty or non existent files
239
                    continue;
×
240
                }
241

242
                context.getLogger().info(" " + RB.$("git.upload.asset"), asset.getFilename());
×
243
            }
×
244
            updateIssues(forgejo, api);
×
245
            return;
×
246
        }
247

248
        if (deleteTags) {
×
249
            deleteTags(api, forgejo.getOwner(), forgejo.getName(), tagName);
×
250
        }
251

252
        // local tag
253
        if (deleteTags || !forgejo.isSkipTag()) {
×
254
            context.getLogger().debug(RB.$("git.releaser.repository.tag"), tagName, context.getModel().getCommit().getShortHash());
×
255
            GitSdk.of(context).tag(tagName, true, context);
×
256
        }
257

258
        // remote tag/release
259
        Release release = new Release();
×
260
        release.setName(forgejo.getEffectiveReleaseName());
×
261
        release.setTagName(tagName);
×
NEW
262
        release.setTargetCommitish(forgejo.getResolvedBranchPush(context));
×
263
        release.setBody(changelog);
×
264
        if (forgejo.getPrerelease().isEnabledSet()) {
×
265
            release.setPrerelease(forgejo.getPrerelease().isEnabled());
×
266
        }
267
        if (forgejo.isDraftSet()) {
×
268
            release.setDraft(forgejo.isDraft());
×
269
        }
270

271
        release = api.createRelease(forgejo.getOwner(), forgejo.getName(), release);
×
272
        api.uploadAssets(forgejo.getOwner(), forgejo.getName(), release, assets);
×
273

274
        if (forgejo.getMilestone().isClose() && !context.getModel().getProject().isSnapshot()) {
×
275
            Optional<Milestone> milestone = api.findMilestoneByName(
×
276
                forgejo.getOwner(),
×
277
                forgejo.getName(),
×
278
                forgejo.getMilestone().getEffectiveName());
×
279
            milestone.ifPresent(m -> api.closeMilestone(forgejo.getOwner(),
×
280
                forgejo.getName(),
×
281
                m));
282
        }
283

284
        updateIssues(forgejo, api);
×
285
    }
×
286

287
    private void updateIssues(org.jreleaser.model.internal.release.ForgejoReleaser forgejo, Forgejo api) throws IOException {
288
        if (!forgejo.getIssues().isEnabled()) return;
×
289

290
        List<String> issueNumbers = ChangelogProvider.getIssues(context);
×
291

292
        if (!issueNumbers.isEmpty()) {
×
293
            context.getLogger().info(RB.$("git.issue.release.mark", issueNumbers.size()));
×
294
        }
295

296
        if (context.isDryrun()) {
×
297
            for (String issueNumber : issueNumbers) {
×
298
                context.getLogger().debug(RB.$("git.issue.release", issueNumber));
×
299
            }
×
300
            return;
×
301
        }
302

NEW
303
        String tagName = forgejo.getEffectiveTagName(context);
×
304
        String labelName = forgejo.getIssues().getLabel().getName();
×
305
        String labelColor = forgejo.getIssues().getLabel().getColor();
×
NEW
306
        TemplateContext props = forgejo.props(context);
×
NEW
307
        forgejo.fillProps(props, context);
×
NEW
308
        String comment = resolveTemplate(context.getLogger(), forgejo.getIssues().getComment(), props);
×
309

310
        if (labelColor.startsWith("#")) {
×
311
            labelColor = labelColor.substring(1);
×
312
        }
313

314
        Label label = null;
×
315

316
        try {
317
            label = api.getOrCreateLabel(
×
318
                forgejo.getOwner(),
×
319
                forgejo.getName(),
×
320
                labelName,
321
                labelColor,
322
                forgejo.getIssues().getLabel().getDescription());
×
323
        } catch (RestAPIException e) {
×
324
            throw new IllegalStateException(RB.$("ERROR_git_releaser_fetch_label", tagName, labelName), e);
×
325
        }
×
326

327
        Optional<Milestone> milestone = Optional.empty();
×
328
        Apply applyMilestone = forgejo.getIssues().getApplyMilestone();
×
329
        if (forgejo.getMilestone().isClose() && !context.getModel().getProject().isSnapshot()) {
×
330
            milestone = api.findMilestoneByName(
×
331
                forgejo.getOwner(),
×
332
                forgejo.getName(),
×
333
                forgejo.getMilestone().getEffectiveName());
×
334

335
            if (!milestone.isPresent()) {
×
336
                milestone = api.findClosedMilestoneByName(
×
337
                    forgejo.getOwner(),
×
338
                    forgejo.getName(),
×
339
                    forgejo.getMilestone().getEffectiveName());
×
340
            }
341
        }
342

343
        for (String issueNumber : issueNumbers) {
×
344
            Optional<Issue> op = api.findIssue(forgejo.getOwner(), forgejo.getName(), Integer.parseInt(issueNumber));
×
345
            if (!op.isPresent()) continue;
×
346

347
            Issue issue = op.get();
×
348
            if ("closed".equals(issue.getState()) && issue.getLabels().stream().noneMatch(l -> l.getName().equals(labelName))) {
×
349
                context.getLogger().debug(RB.$("git.issue.release", issueNumber));
×
350

351
                try {
352
                    api.addLabelToIssue(forgejo.getOwner(), forgejo.getName(), issue, label);
×
353
                    api.commentOnIssue(forgejo.getOwner(), forgejo.getName(), issue, comment);
×
354

355
                    milestone.ifPresent(m -> applyMilestone(forgejo, api, issueNumber, issue, applyMilestone, m));
×
356
                } catch (RestAPIException e) {
×
357
                    if (e.isForbidden()) {
×
358
                        context.getLogger().warn(e.getMessage());
×
359
                    } else {
360
                        throw e;
×
361
                    }
362
                }
×
363
            }
364
        }
×
365
    }
×
366

367
    private void applyMilestone(org.jreleaser.model.internal.release.ForgejoReleaser forgejo, Forgejo api, String issueNumber, Issue issue, Apply applyMilestone, Milestone targetMilestone) {
368
        Milestone issueMilestone = issue.getMilestone();
×
369
        String targetMilestoneTitle = targetMilestone.getTitle();
×
370

371
        if (null == issueMilestone) {
×
372
            context.getLogger().debug(RB.$("git.issue.milestone.apply", targetMilestoneTitle, issueNumber));
×
373
            api.setMilestoneOnIssue(forgejo.getOwner(), forgejo.getName(), issue, targetMilestone);
×
374
        } else {
375
            String milestoneTitle = issueMilestone.getTitle();
×
376

377
            if (applyMilestone == Apply.ALWAYS) {
×
378
                context.getLogger().debug(uncapitalize(RB.$("git.issue.milestone.warn", issueNumber, milestoneTitle)));
×
379
            } else if (applyMilestone == Apply.WARN) {
×
380
                if (!milestoneTitle.equals(targetMilestoneTitle)) {
×
381
                    context.getLogger().warn(RB.$("git.issue.milestone.warn", issueNumber, milestoneTitle));
×
382
                }
383
            } else if (applyMilestone == Apply.FORCE) {
×
384
                if (!milestoneTitle.equals(targetMilestoneTitle)) {
×
385
                    context.getLogger().warn(RB.$("git.issue.milestone.force", targetMilestoneTitle, issueNumber, milestoneTitle));
×
386
                    api.setMilestoneOnIssue(forgejo.getOwner(), forgejo.getName(), issue, targetMilestone);
×
387
                } else {
388
                    context.getLogger().debug(uncapitalize(RB.$("git.issue.milestone.warn", issueNumber, milestoneTitle)));
×
389
                }
390
            }
391
        }
392
    }
×
393

394
    private void updateAssets(Forgejo api, Release release) throws IOException {
395
        Set<org.jreleaser.model.spi.release.Asset> assetsToBeUpdated = new TreeSet<>();
×
396
        Set<org.jreleaser.model.spi.release.Asset> assetsToBeUploaded = new TreeSet<>();
×
397

398
        Map<String, Asset> existingAssets = api.listAssets(forgejo.getOwner(), forgejo.getName(), release);
×
399
        Map<String, org.jreleaser.model.spi.release.Asset> assetsToBePublished = new LinkedHashMap<>();
×
400
        assets.forEach(asset -> assetsToBePublished.put(asset.getFilename(), asset));
×
401

402
        assetsToBePublished.keySet().forEach(name -> {
×
403
            if (existingAssets.containsKey(name)) {
×
404
                assetsToBeUpdated.add(assetsToBePublished.get(name));
×
405
            } else {
406
                assetsToBeUploaded.add(assetsToBePublished.get(name));
×
407
            }
408
        });
×
409

410
        api.updateAssets(forgejo.getOwner(), forgejo.getName(), release, assetsToBeUpdated, existingAssets);
×
411
        api.uploadAssets(forgejo.getOwner(), forgejo.getName(), release, assetsToBeUploaded);
×
412
    }
×
413

414
    private void deleteTags(Forgejo api, String owner, String repo, String tagName) {
415
        // delete remote tag
416
        try {
417
            api.deleteTag(owner, repo, tagName);
×
418
        } catch (RestAPIException ignored) {
×
419
            //noop
420
        }
×
421
    }
×
422
}
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