From 691705c93ab5a890e7937b6691d6d53fbd761852 Mon Sep 17 00:00:00 2001 From: kesha Date: Mon, 16 Mar 2026 00:32:57 -0700 Subject: [PATCH 1/6] Implemented descending order option for tag scanning --- .../github_branch_source/GitHubSCMSource.java | 149 ++++++++++-------- .../GitHubSCMSourceContext.java | 23 +++ .../GitHubSCMSourceRequest.java | 12 ++ .../TagDiscoveryTrait.java | 13 ++ .../TagDiscoveryTrait/config.jelly | 3 + .../help-descendingOrder.html | 5 + .../GithubSCMSourceTagsTest.java | 53 +++++++ .../TagDiscoveryTraitTest.java | 12 ++ 8 files changed, 208 insertions(+), 62 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index ac1c480fc..b98a70d8b 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -2665,10 +2665,12 @@ public int compare(GHBranch o1, GHBranch o2) { static class LazyTags extends LazyIterable { private final GitHubSCMSourceRequest request; private final GHRepository repo; + private final boolean descendingOrder; public LazyTags(GitHubSCMSourceRequest request, GHRepository repo) { this.request = request; this.repo = repo; + this.descendingOrder = request.isTagDescendingOrder(); } @Override @@ -2701,77 +2703,100 @@ protected Iterable create() { // // Instead we just return a wrapped iterator that does the right thing. final Iterable iterable = repo.listRefs("tags"); + + if (descendingOrder) { + // Collect all tags and reverse for descending order. + // This loses lazy-loading but the GitHub REST API does not support + // reverse ordering on refs, so eager collection is unavoidable. + List allTags = collectTags(iterable); + Collections.reverse(allTags); + return allTags; + } + return new Iterable() { @Override public Iterator iterator() { - final Iterator iterator; - try { - iterator = iterable.iterator(); - } catch (Error e) { - if (e.getCause() instanceof GHFileNotFoundException) { - return Collections.emptyIterator(); - } - throw e; - } - return new Iterator() { - boolean hadAtLeastOne; - boolean hasNone; - - @Override - public boolean hasNext() { - try { - boolean hasNext = iterator.hasNext(); - hadAtLeastOne = hadAtLeastOne || hasNext; - return hasNext; - } catch (Error e) { - // pre https://github.com/kohsuke/github-api/commit - // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 - // we at least got the cause, even if wrapped in an Error - if (e.getCause() instanceof GHFileNotFoundException) { - return false; - } - throw e; - } catch (GHException e) { - // JENKINS-52397 I have no clue why https://github.com/kohsuke/github-api/commit - // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 does what it does, but it makes - // it rather difficult to distinguish between a network outage and the file - // not found. - if (hadAtLeastOne) { - throw e; - } - try { - hasNone = hasNone || repo.getRefs("tags").length == 0; - if (hasNone) return false; - throw e; - } catch (FileNotFoundException e1) { - hasNone = true; - return false; - } catch (IOException e1) { - e.addSuppressed(e1); - throw e; - } - } - } - - @Override - public GHRef next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - return iterator.next(); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove"); - } - }; + return safeTagIterator(iterable); } }; } catch (IOException e) { throw new GitHubSCMSource.WrappedException(e); } } + + private List collectTags(Iterable iterable) { + List result = new ArrayList<>(); + Iterator it = safeTagIterator(iterable); + while (it.hasNext()) { + result.add(it.next()); + } + return result; + } + + private Iterator safeTagIterator(Iterable iterable) { + final Iterator iterator; + try { + iterator = iterable.iterator(); + } catch (Error e) { + if (e.getCause() instanceof GHFileNotFoundException) { + return Collections.emptyIterator(); + } + throw e; + } + return new Iterator() { + boolean hadAtLeastOne; + boolean hasNone; + + @Override + public boolean hasNext() { + try { + boolean hasNext = iterator.hasNext(); + hadAtLeastOne = hadAtLeastOne || hasNext; + return hasNext; + } catch (Error e) { + // pre https://github.com/kohsuke/github-api/commit + // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 + // we at least got the cause, even if wrapped in an Error + if (e.getCause() instanceof GHFileNotFoundException) { + return false; + } + throw e; + } catch (GHException e) { + // JENKINS-52397 I have no clue why https://github.com/kohsuke/github-api/commit + // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 does what it does, but it makes + // it rather difficult to distinguish between a network outage and the file + // not found. + if (hadAtLeastOne) { + throw e; + } + try { + hasNone = hasNone || repo.getRefs("tags").length == 0; + if (hasNone) return false; + throw e; + } catch (FileNotFoundException e1) { + hasNone = true; + return false; + } catch (IOException e1) { + e.addSuppressed(e1); + throw e; + } + } + } + + @Override + public GHRef next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return iterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + }; + } } private static class CriteriaWitness implements SCMSourceRequest.Witness { diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java index 6e764c68a..5700b558f 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java @@ -63,6 +63,8 @@ public class GitHubSCMSourceContext extends SCMSourceContext forkPRStrategies = EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); + /** {@code true} if tags should be scanned in descending (reverse alphabetical) order. */ + private boolean tagDescendingOrder; /** {@code true} if notifications should be disabled in this context. */ private boolean notificationsDisabled; /** @@ -102,6 +104,15 @@ public final boolean wantTags() { return wantTags; } + /** + * Returns {@code true} if tags should be scanned in descending order. + * + * @return {@code true} if tags should be scanned in descending order. + */ + public final boolean isTagDescendingOrder() { + return tagDescendingOrder; + } + /** * Returns {@code true} if the {@link GitHubSCMSourceRequest} will need information about pull * requests. @@ -203,6 +214,18 @@ public GitHubSCMSourceContext wantTags(boolean include) { return this; } + /** + * Sets whether tags should be scanned in descending (reverse alphabetical) order. + * + * @param descending {@code true} to scan tags in descending order. + * @return {@code this} for method chaining. + */ + @NonNull + public GitHubSCMSourceContext withTagDescendingOrder(boolean descending) { + tagDescendingOrder = descending; + return this; + } + /** * Adds a requirement for origin pull request details to any {@link GitHubSCMSourceRequest} for * this context. diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java index ea2329eec..374f668ea 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java @@ -59,6 +59,8 @@ public class GitHubSCMSourceRequest extends SCMSourceRequest { private final boolean fetchBranches; /** {@code true} if tag details need to be fetched. */ private final boolean fetchTags; + /** {@code true} if tags should be scanned in descending order. */ + private final boolean tagDescendingOrder; /** {@code true} if origin pull requests need to be fetched. */ private final boolean fetchOriginPRs; /** {@code true} if fork pull requests need to be fetched. */ @@ -124,6 +126,7 @@ public class GitHubSCMSourceRequest extends SCMSourceRequest { super(source, context, listener); fetchBranches = context.wantBranches(); fetchTags = context.wantTags(); + tagDescendingOrder = context.isTagDescendingOrder(); fetchOriginPRs = context.wantOriginPRs(); fetchForkPRs = context.wantForkPRs(); originPRStrategies = fetchOriginPRs && !context.originPRStrategies().isEmpty() @@ -177,6 +180,15 @@ public final boolean isFetchTags() { return fetchTags; } + /** + * Returns {@code true} if tags should be scanned in descending order. + * + * @return {@code true} if tags should be scanned in descending order. + */ + public final boolean isTagDescendingOrder() { + return tagDescendingOrder; + } + /** * Returns {@code true} if pull request details need to be fetched. * diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java index 29b05a5b3..64fac237f 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java @@ -39,6 +39,7 @@ import jenkins.scm.impl.trait.Discovery; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; /** * A {@link Discovery} trait for GitHub that will discover tags on the repository. @@ -46,15 +47,27 @@ * @since 2.3.0 */ public class TagDiscoveryTrait extends SCMSourceTrait { + private boolean descendingOrder; + /** Constructor for stapler. */ @DataBoundConstructor public TagDiscoveryTrait() {} + public boolean isDescendingOrder() { + return descendingOrder; + } + + @DataBoundSetter + public void setDescendingOrder(boolean descendingOrder) { + this.descendingOrder = descendingOrder; + } + /** {@inheritDoc} */ @Override protected void decorateContext(SCMSourceContext context) { GitHubSCMSourceContext ctx = (GitHubSCMSourceContext) context; ctx.wantTags(true); + ctx.withTagDescendingOrder(descendingOrder); ctx.withAuthority(new TagSCMHeadAuthority()); } diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly index d89935c79..4cf2b1463 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly @@ -2,4 +2,7 @@ + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html new file mode 100644 index 000000000..2c5614964 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html @@ -0,0 +1,5 @@ +
+ When checked, tags will be scanned in descending (reverse alphabetical) order + instead of the default ascending order. This is useful when you want the + most recent semantic version tags to be discovered first. +
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java index 806812fe8..4423b7f4c 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java @@ -8,10 +8,12 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; import jenkins.scm.api.SCMHeadObserver; import org.junit.Test; @@ -150,6 +152,57 @@ public void testExistingMultipleTags() throws IOException { } } + @Test + public void testExistingMultipleTagsDescendingOrder() throws IOException { + // Scenario: Requesting multiple tags in descending order + SCMHeadObserver mockSCMHeadObserver = Mockito.mock(SCMHeadObserver.class); + + Mockito.when(mockSCMHeadObserver.getIncludes()) + .thenReturn(new HashSet<>(Arrays.asList( + new GitHubTagSCMHead("existent-multiple-tags1", System.currentTimeMillis()), + new GitHubTagSCMHead("existent-multiple-tags2", System.currentTimeMillis())))); + GitHubSCMSourceContext context = new GitHubSCMSourceContext(null, mockSCMHeadObserver); + context.wantTags(true); + context.withTagDescendingOrder(true); + GitHubSCMSourceRequest request = + context.newRequest(new GitHubSCMSource("cloudbeers", "yolo", null, false), null); + assertTrue(request.isTagDescendingOrder()); + Iterator tags = new GitHubSCMSource.LazyTags(request, repo).iterator(); + + // Expected: Tags should be returned in reverse (descending) order + List tagRefs = new ArrayList<>(); + while (tags.hasNext()) { + tagRefs.add(tags.next().getRef()); + } + assertEquals(2, tagRefs.size()); + assertEquals("refs/tags/existent-multiple-tags2", tagRefs.get(0)); + assertEquals("refs/tags/existent-multiple-tags1", tagRefs.get(1)); + } + + @Test + public void testExistingMultipleTagsAscendingOrderByDefault() throws IOException { + // Scenario: Requesting multiple tags without descending flag (default ascending) + SCMHeadObserver mockSCMHeadObserver = Mockito.mock(SCMHeadObserver.class); + + Mockito.when(mockSCMHeadObserver.getIncludes()) + .thenReturn(new HashSet<>(Arrays.asList( + new GitHubTagSCMHead("existent-multiple-tags1", System.currentTimeMillis()), + new GitHubTagSCMHead("existent-multiple-tags2", System.currentTimeMillis())))); + GitHubSCMSourceContext context = new GitHubSCMSourceContext(null, mockSCMHeadObserver); + context.wantTags(true); + GitHubSCMSourceRequest request = + context.newRequest(new GitHubSCMSource("cloudbeers", "yolo", null, false), null); + assertFalse(request.isTagDescendingOrder()); + Iterator tags = new GitHubSCMSource.LazyTags(request, repo).iterator(); + + // Expected: Tags should be returned in ascending (default) order + assertTrue(tags.hasNext()); + assertEquals("refs/tags/existent-multiple-tags1", tags.next().getRef()); + assertTrue(tags.hasNext()); + assertEquals("refs/tags/existent-multiple-tags2", tags.next().getRef()); + assertFalse(tags.hasNext()); + } + @Test public void testExistingMultipleTagsGHFileNotFoundExceptionIterable() throws IOException { // Scenario: Requesting multiple tags but a FileNotFound is thrown diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java index e4e52e83c..eb31ba735 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java @@ -33,9 +33,21 @@ public void decorateContext() throws Exception { assertThat(probe.wantBranches(), is(false)); assertThat(probe.wantPRs(), is(false)); assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(false)); assertThat(probe.authorities(), contains(instanceOf(TagDiscoveryTrait.TagSCMHeadAuthority.class))); } + @Test + public void decorateContextDescendingOrder() throws Exception { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + assertThat(probe.isTagDescendingOrder(), is(false)); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setDescendingOrder(true); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(true)); + } + @Test public void includeCategory() throws Exception { assertThat(new TagDiscoveryTrait().includeCategory(ChangeRequestSCMHeadCategory.DEFAULT), is(false)); From 4fff8fdd6e9e3bdecbd0df439486d7e84625591e Mon Sep 17 00:00:00 2001 From: kesha Date: Mon, 16 Mar 2026 00:34:53 -0700 Subject: [PATCH 2/6] Add support for specifying descending order scan in global plugin configuration --- .../GitHubConfiguration.java | 21 +++++++++++++ .../TagDiscoveryTrait.java | 20 +++++++++++-- .../GitHubConfiguration/config.jelly | 5 ++++ .../help-tagDescendingOrder.html | 6 ++++ .../help-descendingOrder.html | 4 ++- .../TagDiscoveryTraitTest.java | 30 +++++++++++++++++++ 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-tagDescendingOrder.html diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java index f06c109b2..4e013908b 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java @@ -55,6 +55,8 @@ public static GitHubConfiguration get() { private ApiRateLimitChecker apiRateLimitChecker; + private boolean tagDescendingOrder; + public GitHubConfiguration() { load(); } @@ -83,6 +85,25 @@ public synchronized void setApiRateLimitChecker(@CheckForNull ApiRateLimitChecke save(); } + /** + * Returns whether tags should be scanned in descending order by default. + * + * @return {@code true} if tags should be scanned in descending order by default. + */ + public synchronized boolean isTagDescendingOrder() { + return tagDescendingOrder; + } + + /** + * Sets whether tags should be scanned in descending order by default. + * + * @param tagDescendingOrder {@code true} to scan tags in descending order by default. + */ + public synchronized void setTagDescendingOrder(boolean tagDescendingOrder) { + this.tagDescendingOrder = tagDescendingOrder; + save(); + } + /** * Fix an apiUri. * diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java index 64fac237f..3560b03a7 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java @@ -23,6 +23,7 @@ */ package org.jenkinsci.plugins.github_branch_source; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import jenkins.plugins.git.GitTagSCMRevision; @@ -47,14 +48,27 @@ * @since 2.3.0 */ public class TagDiscoveryTrait extends SCMSourceTrait { - private boolean descendingOrder; + /** + * When {@code null}, the global default from {@link GitHubConfiguration#isTagDescendingOrder()} + * is used. When explicitly set, overrides the global default. + */ + @CheckForNull + private Boolean descendingOrder; /** Constructor for stapler. */ @DataBoundConstructor public TagDiscoveryTrait() {} + /** + * Returns the effective descending order setting, resolving the global default + * when no per-job override has been set. + */ public boolean isDescendingOrder() { - return descendingOrder; + if (descendingOrder != null) { + return descendingOrder; + } + GitHubConfiguration cfg = GitHubConfiguration.get(); + return cfg != null && cfg.isTagDescendingOrder(); } @DataBoundSetter @@ -67,7 +81,7 @@ public void setDescendingOrder(boolean descendingOrder) { protected void decorateContext(SCMSourceContext context) { GitHubSCMSourceContext ctx = (GitHubSCMSourceContext) context; ctx.wantTags(true); - ctx.withTagDescendingOrder(descendingOrder); + ctx.withTagDescendingOrder(isDescendingOrder()); ctx.withAuthority(new TagSCMHeadAuthority()); } diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly index ad20040b0..41f11867a 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly @@ -5,6 +5,11 @@ + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-tagDescendingOrder.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-tagDescendingOrder.html new file mode 100644 index 000000000..28ed2b35b --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-tagDescendingOrder.html @@ -0,0 +1,6 @@ +
+ When checked, tag discovery will scan tags in descending (reverse alphabetical) + order by default across all jobs using GitHub tag discovery. This is useful when you + want the most recent semantic version tags to be discovered first. Individual + jobs can override this setting in their Tag Discovery trait configuration. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html index 2c5614964..6090c3080 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html @@ -1,5 +1,7 @@
When checked, tags will be scanned in descending (reverse alphabetical) order instead of the default ascending order. This is useful when you want the - most recent semantic version tags to be discovered first. + most recent semantic version tags to be discovered first. This setting + overrides the global default configured in Manage Jenkins » System + » Tag Discovery.
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java index eb31ba735..e4fa13534 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java @@ -24,6 +24,7 @@ public class TagDiscoveryTraitTest { @Test public void decorateContext() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(false); GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); assertThat(probe.wantBranches(), is(false)); assertThat(probe.wantPRs(), is(false)); @@ -39,6 +40,7 @@ public void decorateContext() throws Exception { @Test public void decorateContextDescendingOrder() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(false); GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); assertThat(probe.isTagDescendingOrder(), is(false)); TagDiscoveryTrait trait = new TagDiscoveryTrait(); @@ -48,6 +50,34 @@ public void decorateContextDescendingOrder() throws Exception { assertThat(probe.isTagDescendingOrder(), is(true)); } + @Test + public void decorateContextUsesGlobalDefault() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(true); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + new TagDiscoveryTrait().applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(true)); + } finally { + GitHubConfiguration.get().setTagDescendingOrder(false); + } + } + + @Test + public void decorateContextPerJobOverridesGlobal() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(true); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setDescendingOrder(false); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(false)); + } finally { + GitHubConfiguration.get().setTagDescendingOrder(false); + } + } + @Test public void includeCategory() throws Exception { assertThat(new TagDiscoveryTrait().includeCategory(ChangeRequestSCMHeadCategory.DEFAULT), is(false)); From 405904822c311a7f5dfb8ddc9816428b1c3cdfea Mon Sep 17 00:00:00 2001 From: kesha Date: Mon, 16 Mar 2026 00:36:17 -0700 Subject: [PATCH 3/6] Use natural comparator for tag ordering --- pom.xml | 6 ++++++ .../plugins/github_branch_source/GitHubSCMSource.java | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 385e60d87..66b66cb24 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 2.528 2.539 + natural-comparator false true @@ -77,6 +78,11 @@ io.jenkins.plugins okhttp-api + + net.grey-panther + natural-comparator + 1.1 + org.jenkins-ci.plugins credentials diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index b98a70d8b..91de5af18 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -117,6 +117,7 @@ import jenkins.scm.impl.trait.Selection; import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; import jenkins.util.SystemProperties; +import net.greypanther.natsort.SimpleNaturalComparator; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.lib.Constants; import org.jenkinsci.Symbol; @@ -2709,7 +2710,9 @@ protected Iterable create() { // This loses lazy-loading but the GitHub REST API does not support // reverse ordering on refs, so eager collection is unavoidable. List allTags = collectTags(iterable); - Collections.reverse(allTags); + allTags.sort(Comparator.comparing( + (GHRef ref) -> ref.getRef(), + SimpleNaturalComparator.getInstance().reversed())); return allTags; } From 67028fb1317c390faca52c673a1877b895136040 Mon Sep 17 00:00:00 2001 From: kesha Date: Mon, 16 Mar 2026 00:37:21 -0700 Subject: [PATCH 4/6] Add tests to verify natural sorting of tags --- .../GithubSCMSourceTagsTest.java | 29 +++++ .../body-cloudbeers-yolo-versioned.json | 109 ++++++++++++++++++ .../api/__files/body-yolo-versioned-tags.json | 42 +++++++ .../mapping-cloudbeers-yolo-versioned.json | 33 ++++++ .../mappings/mapping-yolo-versioned-tags.json | 33 ++++++ 5 files changed, 246 insertions(+) create mode 100644 src/test/resources/api/__files/body-cloudbeers-yolo-versioned.json create mode 100644 src/test/resources/api/__files/body-yolo-versioned-tags.json create mode 100644 src/test/resources/api/mappings/mapping-cloudbeers-yolo-versioned.json create mode 100644 src/test/resources/api/mappings/mapping-yolo-versioned-tags.json diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java index 4423b7f4c..eecb92c2d 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java @@ -152,6 +152,35 @@ public void testExistingMultipleTags() throws IOException { } } + @Test + public void testNaturalSortDescendingOrder() throws IOException { + // Scenario: Tags with version-like names where natural and alphabetical sort differ. + // Fixture returns alphabetical order: v0.1.11, v0.1.2, v0.10.0, v0.2.0 + // Natural descending should give: v0.10.0, v0.2.0, v0.1.11, v0.1.2 + // (Alphabetical descending would give: v0.2.0, v0.10.0, v0.1.2, v0.1.11 -- wrong) + SCMHeadObserver mockSCMHeadObserver = Mockito.mock(SCMHeadObserver.class); + Mockito.when(mockSCMHeadObserver.getIncludes()).thenReturn(null); + + GHRepository versionedRepo = github.getRepository("cloudbeers/yolo-versioned"); + + GitHubSCMSourceContext context = new GitHubSCMSourceContext(null, mockSCMHeadObserver); + context.wantTags(true); + context.withTagDescendingOrder(true); + GitHubSCMSourceRequest request = + context.newRequest(new GitHubSCMSource("cloudbeers", "yolo-versioned", null, false), null); + Iterator tags = new GitHubSCMSource.LazyTags(request, versionedRepo).iterator(); + + List tagRefs = new ArrayList<>(); + while (tags.hasNext()) { + tagRefs.add(tags.next().getRef()); + } + assertEquals(4, tagRefs.size()); + assertEquals("refs/tags/v0.10.0", tagRefs.get(0)); + assertEquals("refs/tags/v0.2.0", tagRefs.get(1)); + assertEquals("refs/tags/v0.1.11", tagRefs.get(2)); + assertEquals("refs/tags/v0.1.2", tagRefs.get(3)); + } + @Test public void testExistingMultipleTagsDescendingOrder() throws IOException { // Scenario: Requesting multiple tags in descending order diff --git a/src/test/resources/api/__files/body-cloudbeers-yolo-versioned.json b/src/test/resources/api/__files/body-cloudbeers-yolo-versioned.json new file mode 100644 index 000000000..14a2a2136 --- /dev/null +++ b/src/test/resources/api/__files/body-cloudbeers-yolo-versioned.json @@ -0,0 +1,109 @@ +{ + "id": 43041241, + "name": "yolo-versioned", + "full_name": "cloudbeers/yolo-versioned", + "owner": { + "login": "cloudbeers", + "id": 4181899, + "avatar_url": "https://avatars.githubusercontent.com/u/4181899?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/cloudbeers", + "html_url": "https://github.com/cloudbeers", + "followers_url": "https://api.github.com/users/cloudbeers/followers", + "following_url": "https://api.github.com/users/cloudbeers/following{/other_user}", + "gists_url": "https://api.github.com/users/cloudbeers/gists{/gist_id}", + "starred_url": "https://api.github.com/users/cloudbeers/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/cloudbeers/subscriptions", + "organizations_url": "https://api.github.com/users/cloudbeers/orgs", + "repos_url": "https://api.github.com/users/cloudbeers/repos", + "events_url": "https://api.github.com/users/cloudbeers/events{/privacy}", + "received_events_url": "https://api.github.com/users/cloudbeers/received_events", + "type": "Organization", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/cloudbeers/yolo-versioned", + "description": "Versioned tags test repo", + "fork": false, + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned", + "forks_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/forks", + "keys_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/teams", + "hooks_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/hooks", + "issue_events_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/issues/events{/number}", + "events_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/events", + "assignees_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/assignees{/user}", + "branches_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/branches{/branch}", + "tags_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/tags", + "blobs_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/statuses/{sha}", + "languages_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/languages", + "stargazers_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/stargazers", + "contributors_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/contributors", + "subscribers_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/subscribers", + "subscription_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/subscription", + "commits_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/contents/{+path}", + "compare_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/merges", + "archive_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/downloads", + "issues_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/issues{/number}", + "pulls_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/pulls{/number}", + "milestones_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/milestones{/number}", + "notifications_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/labels{/name}", + "releases_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/releases{/id}", + "deployments_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/deployments", + "created_at": "2015-09-24T02:58:30Z", + "updated_at": "2016-12-07T23:55:35Z", + "pushed_at": "2016-12-01T16:07:01Z", + "git_url": "git://github.com/cloudbeers/yolo-versioned.git", + "ssh_url": "git@github.com:cloudbeers/yolo-versioned.git", + "clone_url": "https://github.com/cloudbeers/yolo-versioned.git", + "svn_url": "https://github.com/cloudbeers/yolo-versioned", + "homepage": null, + "size": 3, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 0, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "organization": { + "login": "cloudbeers", + "id": 4181899, + "avatar_url": "https://avatars.githubusercontent.com/u/4181899?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/cloudbeers", + "html_url": "https://github.com/cloudbeers", + "followers_url": "https://api.github.com/users/cloudbeers/followers", + "following_url": "https://api.github.com/users/cloudbeers/following{/other_user}", + "gists_url": "https://api.github.com/users/cloudbeers/gists{/gist_id}", + "starred_url": "https://api.github.com/users/cloudbeers/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/cloudbeers/subscriptions", + "organizations_url": "https://api.github.com/users/cloudbeers/orgs", + "repos_url": "https://api.github.com/users/cloudbeers/repos", + "events_url": "https://api.github.com/users/cloudbeers/events{/privacy}", + "received_events_url": "https://api.github.com/users/cloudbeers/received_events", + "type": "Organization", + "site_admin": false + }, + "network_count": 0, + "subscribers_count": 0 +} diff --git a/src/test/resources/api/__files/body-yolo-versioned-tags.json b/src/test/resources/api/__files/body-yolo-versioned-tags.json new file mode 100644 index 000000000..4e29256db --- /dev/null +++ b/src/test/resources/api/__files/body-yolo-versioned-tags.json @@ -0,0 +1,42 @@ +[ + { + "ref": "refs/tags/v0.1.11", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjEuMTE=", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.1.11", + "object": { + "sha": "aa11111111111111111111111111111111111111", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/aa11111111111111111111111111111111111111" + } + }, + { + "ref": "refs/tags/v0.1.2", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjEuMg==", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.1.2", + "object": { + "sha": "bb22222222222222222222222222222222222222", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/bb22222222222222222222222222222222222222" + } + }, + { + "ref": "refs/tags/v0.2.0", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjIuMA==", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.2.0", + "object": { + "sha": "cc33333333333333333333333333333333333333", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/cc33333333333333333333333333333333333333" + } + }, + { + "ref": "refs/tags/v0.10.0", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjEwLjA=", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.10.0", + "object": { + "sha": "dd44444444444444444444444444444444444444", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/dd44444444444444444444444444444444444444" + } + } +] diff --git a/src/test/resources/api/mappings/mapping-cloudbeers-yolo-versioned.json b/src/test/resources/api/mappings/mapping-cloudbeers-yolo-versioned.json new file mode 100644 index 000000000..4c520ce47 --- /dev/null +++ b/src/test/resources/api/mappings/mapping-cloudbeers-yolo-versioned.json @@ -0,0 +1,33 @@ +{ + "request": { + "url": "/repos/cloudbeers/yolo-versioned", + "method": "GET" + }, + "response": { + "status": 200, + "bodyFileName": "body-cloudbeers-yolo-versioned.json", + "headers": { + "Server": "GitHub.com", + "Date": "Tue, 06 Dec 2016 15:06:25 GMT", + "Content-Type": "application/json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Status": "200 OK", + "X-RateLimit-Limit": "600", + "X-RateLimit-Remaining": "600", + "X-RateLimit-Reset": "1481039662", + "Cache-Control": "public, max-age=60, s-maxage=60", + "Vary": ["Accept", "Accept-Encoding"], + "ETag": "W/\"12cee9e1d9874cbabcbfaf3b112e8dac\"", + "X-GitHub-Media-Type": "github.v3; format=json", + "Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", + "Access-Control-Allow-Origin": "*", + "Content-Security-Policy": "default-src 'none'", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "deny", + "X-XSS-Protection": "1; mode=block", + "X-Served-By": "2d7a5e35115884240089368322196939", + "X-GitHub-Request-Id": "B2A7FE77:629C:AF96C44:5846D3F1" + } + } +} diff --git a/src/test/resources/api/mappings/mapping-yolo-versioned-tags.json b/src/test/resources/api/mappings/mapping-yolo-versioned-tags.json new file mode 100644 index 000000000..76c9af5de --- /dev/null +++ b/src/test/resources/api/mappings/mapping-yolo-versioned-tags.json @@ -0,0 +1,33 @@ +{ + "request": { + "url": "/repos/cloudbeers/yolo-versioned/git/refs/tags", + "method": "GET" + }, + "response": { + "status": 200, + "bodyFileName": "body-yolo-versioned-tags.json", + "headers": { + "Server": "GitHub.com", + "Date": "Mon, 29 Jul 2019 23:22:30 GMT", + "Content-Type": "application/json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Status": "200 OK", + "X-RateLimit-Limit": "600", + "X-RateLimit-Remaining": "595", + "X-RateLimit-Reset": "1481048932", + "Cache-Control": "public, max-age=60, s-maxage=60", + "Vary": ["Accept", "Accept-Encoding"], + "ETag": "W/\"a466888c00a5eaf3354ccabe37f75dd9\"", + "X-GitHub-Media-Type": "github.v3; format=json", + "Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", + "Access-Control-Allow-Origin": "*", + "Content-Security-Policy": "default-src 'none'", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "deny", + "X-XSS-Protection": "1; mode=block", + "X-Served-By": "88531cdcf1929112ec480e1806d44a33", + "X-GitHub-Request-Id": "BC8D23FA:31E4:269B4E33:5846F623" + } + } +} From 9e9b2bac518e24a913d41180c322443dc2f4491c Mon Sep 17 00:00:00 2001 From: kesha Date: Mon, 16 Mar 2026 00:40:03 -0700 Subject: [PATCH 5/6] Implemented support for maximum number of tags to fetch when using descending order --- .../GitHubConfiguration.java | 21 ++++++++++ .../github_branch_source/GitHubSCMSource.java | 8 ++++ .../GitHubSCMSourceContext.java | 23 +++++++++++ .../GitHubSCMSourceRequest.java | 12 ++++++ .../TagDiscoveryTrait.java | 25 ++++++++++++ .../GitHubConfiguration/config.jelly | 8 ++-- .../GitHubConfiguration/help-maxTagCount.html | 6 +++ .../TagDiscoveryTrait/config.jelly | 8 ++-- .../TagDiscoveryTrait/help-maxTagCount.html | 6 +++ .../TagDiscoveryTraitTest.java | 40 +++++++++++++++++++ 10 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-maxTagCount.html create mode 100644 src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-maxTagCount.html diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java index 4e013908b..cf89444ba 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java @@ -57,6 +57,8 @@ public static GitHubConfiguration get() { private boolean tagDescendingOrder; + private int maxTagCount; + public GitHubConfiguration() { load(); } @@ -104,6 +106,25 @@ public synchronized void setTagDescendingOrder(boolean tagDescendingOrder) { save(); } + /** + * Returns the default maximum number of tags to process (0 = unlimited). + * + * @return the default maximum number of tags to process. + */ + public synchronized int getMaxTagCount() { + return maxTagCount; + } + + /** + * Sets the default maximum number of tags to process. + * + * @param maxTagCount maximum number of tags (0 = unlimited). + */ + public synchronized void setMaxTagCount(int maxTagCount) { + this.maxTagCount = maxTagCount; + save(); + } + /** * Fix an apiUri. * diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index 91de5af18..74b612ad0 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -1186,6 +1186,14 @@ public SCMSourceCriteria.Probe create( listener.getLogger().format("%n %d tags were processed (query completed)%n", count); break; } + int maxTagCount = request.getMaxTagCount(); + if (maxTagCount > 0 && count >= maxTagCount) { + listener.getLogger() + .format( + "%n %d tags were processed (limit reached)%n", + count); + break; + } } listener.getLogger().format("%n %d tags were processed%n", count); } diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java index 5700b558f..da939fe10 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java @@ -65,6 +65,8 @@ public class GitHubSCMSourceContext extends SCMSourceContext forkPRStrategies = EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); /** {@code true} if tags should be scanned in descending (reverse alphabetical) order. */ private boolean tagDescendingOrder; + /** Maximum number of tags to process (0 = unlimited). */ + private int maxTagCount; /** {@code true} if notifications should be disabled in this context. */ private boolean notificationsDisabled; /** @@ -113,6 +115,15 @@ public final boolean isTagDescendingOrder() { return tagDescendingOrder; } + /** + * Returns the maximum number of tags to process (0 = unlimited). + * + * @return the maximum number of tags to process. + */ + public final int getMaxTagCount() { + return maxTagCount; + } + /** * Returns {@code true} if the {@link GitHubSCMSourceRequest} will need information about pull * requests. @@ -226,6 +237,18 @@ public GitHubSCMSourceContext withTagDescendingOrder(boolean descending) { return this; } + /** + * Sets the maximum number of tags to process. + * + * @param maxTagCount maximum number of tags (0 = unlimited). + * @return {@code this} for method chaining. + */ + @NonNull + public GitHubSCMSourceContext withMaxTagCount(int maxTagCount) { + this.maxTagCount = maxTagCount; + return this; + } + /** * Adds a requirement for origin pull request details to any {@link GitHubSCMSourceRequest} for * this context. diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java index 374f668ea..af5e9d038 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java @@ -61,6 +61,8 @@ public class GitHubSCMSourceRequest extends SCMSourceRequest { private final boolean fetchTags; /** {@code true} if tags should be scanned in descending order. */ private final boolean tagDescendingOrder; + /** Maximum number of tags to process (0 = unlimited). */ + private final int maxTagCount; /** {@code true} if origin pull requests need to be fetched. */ private final boolean fetchOriginPRs; /** {@code true} if fork pull requests need to be fetched. */ @@ -127,6 +129,7 @@ public class GitHubSCMSourceRequest extends SCMSourceRequest { fetchBranches = context.wantBranches(); fetchTags = context.wantTags(); tagDescendingOrder = context.isTagDescendingOrder(); + maxTagCount = context.getMaxTagCount(); fetchOriginPRs = context.wantOriginPRs(); fetchForkPRs = context.wantForkPRs(); originPRStrategies = fetchOriginPRs && !context.originPRStrategies().isEmpty() @@ -189,6 +192,15 @@ public final boolean isTagDescendingOrder() { return tagDescendingOrder; } + /** + * Returns the maximum number of tags to process (0 = unlimited). + * + * @return the maximum number of tags to process. + */ + public final int getMaxTagCount() { + return maxTagCount; + } + /** * Returns {@code true} if pull request details need to be fetched. * diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java index 3560b03a7..52251b623 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java @@ -55,6 +55,13 @@ public class TagDiscoveryTrait extends SCMSourceTrait { @CheckForNull private Boolean descendingOrder; + /** + * When {@code null}, the global default from {@link GitHubConfiguration#getMaxTagCount()} + * is used. When explicitly set, overrides the global default. + */ + @CheckForNull + private Integer maxTagCount; + /** Constructor for stapler. */ @DataBoundConstructor public TagDiscoveryTrait() {} @@ -76,12 +83,30 @@ public void setDescendingOrder(boolean descendingOrder) { this.descendingOrder = descendingOrder; } + /** + * Returns the effective max tag count, resolving the global default + * when no per-job override has been set. + */ + public int getMaxTagCount() { + if (maxTagCount != null) { + return maxTagCount; + } + GitHubConfiguration cfg = GitHubConfiguration.get(); + return cfg != null ? cfg.getMaxTagCount() : 0; + } + + @DataBoundSetter + public void setMaxTagCount(int maxTagCount) { + this.maxTagCount = maxTagCount; + } + /** {@inheritDoc} */ @Override protected void decorateContext(SCMSourceContext context) { GitHubSCMSourceContext ctx = (GitHubSCMSourceContext) context; ctx.wantTags(true); ctx.withTagDescendingOrder(isDescendingOrder()); + ctx.withMaxTagCount(getMaxTagCount()); ctx.withAuthority(new TagSCMHeadAuthority()); } diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly index 41f11867a..556ee6518 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly @@ -6,9 +6,11 @@
- - - + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-maxTagCount.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-maxTagCount.html new file mode 100644 index 000000000..a615ecbf7 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-maxTagCount.html @@ -0,0 +1,6 @@ +
+ Limits the number of tags to process during tag discovery. Set to 0 + (the default) for no limit. This setting is only effective when + descending order is enabled. Individual jobs can override this setting + in their Tag Discovery trait configuration. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly index 4cf2b1463..40eb02f23 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly @@ -2,7 +2,9 @@ - - - + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-maxTagCount.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-maxTagCount.html new file mode 100644 index 000000000..7c432caae --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-maxTagCount.html @@ -0,0 +1,6 @@ +
+ Limits the number of tags to process during tag discovery. Set to 0 + (the default) for no limit. This setting is only effective when + descending order is enabled. This setting overrides the global default + configured in Manage Jenkins » System » Tag Discovery. +
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java index e4fa13534..bc9ac4714 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java @@ -78,6 +78,46 @@ public void decorateContextPerJobOverridesGlobal() throws Exception { } } + @Test + public void decorateContextMaxTagCount() throws Exception { + GitHubConfiguration.get().setMaxTagCount(0); + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + assertThat(probe.getMaxTagCount(), is(0)); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setMaxTagCount(10); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.getMaxTagCount(), is(10)); + } + + @Test + public void decorateContextMaxTagCountUsesGlobalDefault() throws Exception { + GitHubConfiguration.get().setMaxTagCount(25); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + new TagDiscoveryTrait().applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.getMaxTagCount(), is(25)); + } finally { + GitHubConfiguration.get().setMaxTagCount(0); + } + } + + @Test + public void decorateContextMaxTagCountPerJobOverridesGlobal() throws Exception { + GitHubConfiguration.get().setMaxTagCount(25); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setMaxTagCount(5); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.getMaxTagCount(), is(5)); + } finally { + GitHubConfiguration.get().setMaxTagCount(0); + } + } + @Test public void includeCategory() throws Exception { assertThat(new TagDiscoveryTrait().includeCategory(ChangeRequestSCMHeadCategory.DEFAULT), is(false)); From 4d9e923b26eff146b2085bc27d3fa93c6f41c48a Mon Sep 17 00:00:00 2001 From: kesha Date: Wed, 18 Mar 2026 16:43:18 -0700 Subject: [PATCH 6/6] Fix spotless:check complaint --- .../plugins/github_branch_source/GitHubSCMSource.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index 74b612ad0..1c18e8549 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -1188,10 +1188,7 @@ public SCMSourceCriteria.Probe create( } int maxTagCount = request.getMaxTagCount(); if (maxTagCount > 0 && count >= maxTagCount) { - listener.getLogger() - .format( - "%n %d tags were processed (limit reached)%n", - count); + listener.getLogger().format("%n %d tags were processed (limit reached)%n", count); break; } }