Ok, so that people can see code rather than just yapping on the M-L (and given that we seem to be OK with the annotation names)
https://github.com/stapler/stapler/pull/121 Is where I am working on this proposal. We can continue the discussion in either place as appropriate (general comments here, specific comments on specific details of the code in the PR) On 7 July 2017 at 03:06, Stephen Connolly <[email protected]> wrote: > Yes I was thinking that a custom javadoc handler would be useful. > Especially as there are inheritance of facets and fragments that we'd want > to see documented correctly. > > On 7 July 2017 at 02:37, Robert Sandell <[email protected]> wrote: > >> I agree with Stephen here; adding methods seems risky and could break >> existing plugins, at least in a backwards compat sense. >> >> But I at the same time do like the idea of getting javadoc for the facets >> and fragments. So can we get both somehow? >> E.g. if there is a @StaplerFacet("login") or @StaplerFragment("main") there >> should be a corresponding >> >> @facet login the login page. >> @fragment main add this to change the main part of the configuration page. >> >> in the javadoc of the class? >> >> /B >> >> 2017-07-06 13:16 GMT+02:00 Stephen Connolly < >> [email protected]>: >> >>> >>> >>> On 5 July 2017 at 18:10, Kohsuke Kawaguchi <[email protected]> wrote: >>> >>>> FWIW, I've already discussed this offline with Stephen and I'm >>>> generally +1. >>>> >>>> One suggestion I had for him is to consider using a Java method >>>> definition instead of @StaplerFacet and @StaplerFragment. That is, instead >>>> of this: >>>> >>>> @StaplerFacets({ >>>> @StaplerFacet("index"), >>>> @StaplerFacet("config") >>>> }) >>>> @StaplerFragments({ >>>> @StaplerFragment("main"), >>>> @StaplerFragment("side-panel"), >>>> @StaplerFragment(value="tasks",optional=true) >>>> }) >>>> public class Widget { ... } >>>> >>>> Consider that: >>>> >>>> public class Widget { >>>> @StaplerFacet protected void index() {} >>>> @StaplerFacet protected void config() {} >>>> >>>> @StaplerFragment protected abstract void main(); >>>> @StaplerFragment("side-panel") protected abstract void sidepanel(); >>>> @StaplerFragment protected void tasks(); >>>> } >>>> >>>> >>> Well at least this is a better explanation than you gave me verbally ;-) >>> >>> >>>> I liked this better because ... >>>> >>>> - It fits with the original design thinking behind Stapler, which >>>> is that views are like methods. Their inheritance and override semantics >>>> are all modeled after methods. >>>> - It avoids a long list of @StaplerFragment/@StaplerFacet at the >>>> top of the class declaration. We have some objects in the core that has >>>> quite a few views. >>>> >>>> So let's see: >>> >>> $ ( cd core/src/main/resources ; find . -name \*.jelly -exec dirname {} >>> \; ; find . -name \*.groovy -exec dirname {} \; ) | sort | uniq -c | grep >>> -v ./lib/ | sort -n | tail -n 10 >>> 7 ./hudson/model/AbstractProject >>> 7 ./hudson/slaves/SlaveComputer >>> 7 ./jenkins/install/SetupWizard >>> 9 ./hudson/model/Run >>> 11 ./hudson/PluginManager >>> 11 ./hudson/security/HudsonPrivateSecurityRealm >>> 12 ./hudson/model/Job >>> 13 ./hudson/model/View >>> 14 ./hudson/model/Computer >>> 25 ./jenkins/model/Jenkins >>> >>> So there are not altogether too many classes in Jenkins having lots of >>> fragments / facets >>> >>> I think an argument based on a couple of really big classes like Jenkins >>> having 25 lines of >>> >>> @StaplerObject >>> @StaplerFacet("_restart") >>> @StaplerFacet("_safeRestart") >>> @StaplerFacet("_script") >>> @StaplerFacet("_scriptText") >>> @StaplerFacet("accessDenied") >>> @StaplerFacet("configure") >>> @StaplerFacet("configureExecutors") >>> @StaplerFacet("fingerprintCheck") >>> @StaplerFacet("legend") >>> @StaplerFacet("load-statistics") >>> @StaplerFacet("login") >>> @StaplerFacet("loginError") >>> @StaplerFacet("manag_") >>> @StaplerFacet("manage") >>> @StaplerFacet("newView") >>> @StaplerFacet("noPrincipal") >>> @StaplerFacet("oops") >>> @StaplerFacet("opensearch.xml") >>> @StaplerFacet("projectRelationship-help") >>> @StaplerFacet("projectRelationship") >>> @StaplerFacet("systemInfo") >>> @StaplerFacet("threadDump") >>> @StaplerFragment("_api") >>> @StaplerFragment("downgrade") >>> @StaplerFragment("sidepanel") >>> public class Jenkins { >>> ... >>> } >>> >>> compared with >>> >>> @StaplerObject >>> public class Jenkins { >>> @StaplerFacet protected void _restart() {} >>> @StaplerFacet protected void _safeRestart() {} >>> @StaplerFacet protected void _script() {} >>> @StaplerFacet protected void _scriptText() {} >>> @StaplerFacet protected void accessDenied() {} >>> @StaplerFacet protected void configure() {} >>> @StaplerFacet protected void configureExecutors() {} >>> @StaplerFacet protected void fingerprintCheck() {} >>> @StaplerFacet protected void legend() {} >>> @StaplerFacet @StaplerPath("load-statistics") protected void >>> loadStatistics() {} >>> @StaplerFacet protected void login() {} >>> @StaplerFacet protected void loginError() {} >>> @StaplerFacet protected void manag_() {} >>> @StaplerFacet protected void manage() {} >>> @StaplerFacet protected void newView() {} >>> @StaplerFacet protected void noPrincipal() {} >>> @StaplerFacet protected void oops() {} >>> @StaplerFacet @StaplerPath("opensearch.xml") protected void >>> opensearchXml() {} >>> @StaplerFacet @StaplerPath("projectRelationship-help") protected void >>> projectRelationshipHelp() >>> @StaplerFacet protected void projectRelationship() {} >>> @StaplerFacet protected void systemInfo() {} >>> @StaplerFacet protected void threadDump() {} >>> @StaplerFragment protected void _api() {} >>> @StaplerFragment protected void downgrade() {} >>> @StaplerFragment protected void sidepanel() {} >>> ... >>> } >>> >>> just to get the javadoc comments and leverage abstract and final... I >>> really do not like the extra effort of typing in all those method names... >>> >>> Perhaps the biggest issue for me is that to retrofit that into Jenkins >>> we would need to take great care that these methods we are adding as facet >>> and fragment markers do not have names that may potentially conflict with >>> subclasses... >>> >>> If we add >>> >>> @StaplerObject >>> public abstract class Descriptor ... { >>> ... >>> @StaplerFragment protected abstract void config(); // doesn't really >>> matter if abstract or not here >>> ... >>> } >>> >>> we may make it impossible for a concrete subclass to have a >>> >>> public MyDescriptor extends Descriptor ... { >>> >>> private MyConfig config; >>> >>> ... >>> >>> public MyConfig config() { >>> return config.clone(); >>> } >>> >>> } >>> >>> as they will be unable to retain binary compatibility with their >>> existing "config" method when they upgrade... >>> >>> So that means we will need to do >>> >>> @StaplerObject >>> public abstract class Descriptor ... { >>> ... >>> @StaplerFragment @StaplerPath("config") protected abstract void >>> _retrofit_config(); >>> ... >>> } >>> >>> Thinking some more, I see issues with plugin authors being thereby >>> forced to add implementations when they upgrade core... ok so they are >>> quick and the IDE will help... but having to add all those >>> >>> public MyDescriptor extends Descriptor ... { >>> >>> private MyConfig config; >>> >>> ... >>> >>> public MyConfig config() { >>> return config.clone(); >>> } >>> >>> @StaplerFragment @StaplerPath("config") protected void >>> _retrofit_config() {} >>> } >>> >>> All the while hoping that the IDE has copied the method annotations >>> (otherwise the method is not annotated) and the plugin author has not opted >>> into @StaplerObject yet >>> >>> That, to me, does not make for a compelling developer experience. >>> >>> WDYT? >>> >>> I am inclined to reject your method based idea and stick with class >>> level annotations... there are only 11 classes in all of Jenkins core with >>> more than 5 facets / fragments >>> >>> >>>> - It creates a nice to place to describe view as javadoc. >>>> - 'abstract' and 'final' provides convenient semantics and javac >>>> does that work for us. One less thing to do for our annotation processor >>>> >>>> Obvious downside is that those are not real methods. >>>> >>>> On Mon, Jul 3, 2017 at 5:21 AM Stephen Connolly < >>>> [email protected]> wrote: >>>> >>>>> I have been developing Jenkins plugins and changes to Jenkins core for >>>>> more than 10 years now. As such, I have internalized a lot of the >>>>> knowledge >>>>> about how to develop against Jenkins and more specifically against >>>>> Stapler. >>>>> >>>>> When I talk to people trying to start out development against Jenkins, >>>>> the magic of Stapler seems to be a big source of confusion - at least to >>>>> the people I have talked to. >>>>> >>>>> Prospective developers get confused: >>>>> >>>>> - between primary facets and facet fragments. >>>>> - where to put facets and facet fragments. >>>>> - which facet fragments are optional and which ones are mandatory >>>>> >>>>> (Did you even know that those jelly / groovy views are called facets >>>>> and that there are two kinds?) >>>>> >>>>> Some of those concerns affect me too... but I am a seasoned Jenkins >>>>> developer CMD+SHIFT+P, *, ENTER and IntelliJ is showing me a full list of >>>>> facets and facet fragments from the class hierarchy and I can search each >>>>> one looking for the <st:include page="fragment" optional="true"> to >>>>> discover that there is an optional facet fragment... and then try and dig >>>>> through the Jelly / Groovy logic to determine when and why that fragment >>>>> should be included. >>>>> >>>>> It doesn't just stop there... There are those methods that we expect >>>>> Stapler to invoke for us... typically they are the doFillXYZItems() >>>>> or doCheckXYZ() methods. I find myself having to mark them up like: >>>>> >>>>> @SuppressWarnings("unused") // stapler >>>>> public FormValidation doCheckWidgetValue(@QueryParameter String >>>>> value) { >>>>> ... >>>>> } >>>>> >>>>> >>>>> So that my IDE stops complaining about the dead code. It's not a big >>>>> deal in the grand scheme of things, but I'd much rather have an annotation >>>>> on the method to signify that we expect the method to be used by >>>>> stapler... >>>>> Right now I could do that using >>>>> >>>>> @WebMethod(name="checkWidgetValue") >>>>> public FormValidation doCheckWidgetValue(@QueryParameter String >>>>> value) { >>>>> ... >>>>> } >>>>> >>>>> >>>>> If I don't like that, I guess I could use the newer HTTP verb >>>>> annotations in stapler: >>>>> >>>>> @GET // stapler >>>>> public FormValidation doCheckWidgetValue(@QueryParameter String >>>>> value) { >>>>> ... >>>>> } >>>>> >>>>> >>>>> But the simple @GET is not screaming out stapler to me, so I feel >>>>> compelled to add a // stapler comment to remind myself that the >>>>> annotation is used by stapler >>>>> >>>>> Then we hit the actual name that stapler exposes things on. There are >>>>> some conventions that stapler follows... and they are magic conventions... >>>>> so much so that I just keep http://stapler.kohsuke.org/reference.html >>>>> as the first bookmark in my bookmark bar. >>>>> >>>>> I think we can do better. I think we should do better, and here is my >>>>> proposal. >>>>> >>>>> We should introduce some annotations. Make them available against >>>>> older versions of Jenkins so that we don't have to wait for everyone to >>>>> update their plugin baseline. The annotations can be added as a plugin >>>>> dependency in the parent pom, that everyone can use them just by upgrading >>>>> the plugin parent pom. >>>>> >>>>> Here are the class annotations I think we need: >>>>> >>>>> - An annotation (I propose @StaplerObject) that says "This object >>>>> is expected to bound to a URL by Stapler, the Stapler annotations are >>>>> complete, check that there are no facets / fragments without a >>>>> corresponding annotation and no magic on this class please" (so if >>>>> there >>>>> is, say a rename.jelly but no @StaplerFacet("rename") or >>>>> @StaplerFragment("rename") then you get an error) >>>>> - An annotation (I propose @StaplerFacet and the repeatable >>>>> container @StaplerFacets) that says "Here are the facets that this >>>>> object is expected to have" (doesn't have to be on a @StaplerObject >>>>> but when not on a @StaplerObject we can only check for missing >>>>> expected facets, we cannot check for "unexpected" facets - i.e. I >>>>> created a >>>>> facet with a spelling mistake or typo in the name) >>>>> - An annotation (I propose @StaplerFragment and the repeatable >>>>> container @StaplerFragments) that says "Here are the facet >>>>> fragments that this object is required or may optionally have" (quite >>>>> often >>>>> will not be on a @StaplerObject) >>>>> >>>>> >>>>> So I envision something like this (pre-Java 8): >>>>> >>>>> @StaplerObject >>>>> @StaplerFacets({ >>>>> @StaplerFacet("index"), >>>>> @StaplerFacet("config") >>>>> }) >>>>> @StaplerFragments({ >>>>> @StaplerFragment("main"), >>>>> @StaplerFragment("side-panel"), >>>>> @StaplerFragment(value="tasks",optional=true) >>>>> }) >>>>> public class Widget { ... } >>>>> >>>>> >>>>> When compiled targeting Java 8 this would become: >>>>> >>>>> @StaplerObject >>>>> @StaplerFacet("index") >>>>> @StaplerFacet("config") >>>>> @StaplerFragment("main") >>>>> @StaplerFragment("side-panel") >>>>> @StaplerFragment(value="tasks",optional=true) >>>>> public class Widget { ... } >>>>> >>>>> >>>>> There would be an annotation processor that could do some checks: >>>>> >>>>> - If you add @StaplerFragment(..., optional=false) then any >>>>> non-abstract class must have that fragment somewhere in the class >>>>> hierarchy >>>>> - If you add @StaplerFacet then any non-abstract class must have >>>>> that fragment somewhere in the class hierarchy >>>>> - If you add @StaplerObject then only the named facets and facet >>>>> fragments must be present for that class (to catch somebody calling the >>>>> facet resource with an incorrect name - it should be >>>>> side-panel2.jelly but they called it sidepanel2.jelly) >>>>> >>>>> >>>>> Then, while we are at it, I'd also like to add method / field >>>>> annotations: >>>>> >>>>> - An annotation (I propose @StaplerPath and the repeatable >>>>> container @StaplerPaths) that says "here is the URL segment names >>>>> that this method / field matches when Stapler is binding the URL". This >>>>> annotation can be used to enable easier refactoring without risk of >>>>> breaking the URLs because we can rename the getters and keep the URL >>>>> scheme >>>>> as before or even keep the legacy URLs but direct to the new URLs via >>>>> the >>>>> UI. >>>>> - Annotations (I propose @StaplerHEAD / @StaplerGET / @StaplerPOST >>>>> / @StaplerPUT / @StaplerPATCH / @StaplerDELETE plus non standard >>>>> methods supported with @StaplerMethod("...") and then a repeatable >>>>> container of @StaplerMethods - the repeatable container would only >>>>> take @StaplerMethod annotations though) that cover the different >>>>> HTTP methods (a.k.a. verbs) - we have some existing, but they need some >>>>> extensions and I think it might be a good idea to consolidate the >>>>> names, >>>>> especially if we want to allow consumption on older baseline versions >>>>> of >>>>> Jenkins >>>>> >>>>> >>>>> So this could be something like this (Pre Java 8): >>>>> >>>>> public class Widget { >>>>> @StaplerPath >>>>> public Manchu manchu; >>>>> >>>>> @StaplerPaths({@StaplerPath,@StaplerPath("fu")}) >>>>> public Foo foo; >>>>> >>>>> @StaplerPath >>>>> public Bar getBar() { ... } >>>>> >>>>> @StaplerPath(StaplerPath.DYNAMIC) >>>>> public Object getDynamic(StaplerRequest req) { ... } >>>>> >>>>> @StaplerPOST >>>>> public HttpResponse doActivate(StaplerRequest req) { ... } >>>>> >>>>> @StaplerPath(StaplerPath.DYNAMIC) >>>>> @StaplerPUT >>>>> public HttpResponse doDynamic(StaplerRequest req) { ... } >>>>> } >>>>> >>>>> >>>>> Or targeting Java 8: >>>>> >>>>> public class Widget { >>>>> @StaplerPath >>>>> public Manchu manchu; >>>>> >>>>> @StaplerPath >>>>> @StaplerPath("fu") >>>>> public Foo foo; >>>>> >>>>> @StaplerPath >>>>> public Bar getBar() { ... } >>>>> >>>>> @StaplerPath(StaplerPath.DYNAMIC) >>>>> public Object getDynamic(StaplerRequest req) { ... } >>>>> >>>>> @StaplerPOST >>>>> public HttpResponse doActivate(StaplerRequest req) { ... } >>>>> >>>>> @StaplerPath(StaplerPath.DYNAMIC) >>>>> @StaplerPUT >>>>> public HttpResponse doDynamic(StaplerRequest req) { ... } >>>>> } >>>>> >>>>> >>>>> The annotation processor can then give additional checks: >>>>> >>>>> >>>>> - @StaplerPath annotated fields must be public >>>>> - @StaplerPath(altName) annotated fields are only allowed if >>>>> targeting a new version Stapler that adds support for that (unless the >>>>> name >>>>> is the inferred name) >>>>> - @StaplerPath annotated methods must be public and either match >>>>> one of the allowed getter signatures or also have a HTTP method >>>>> annotation >>>>> and match the allowed action method signatures. >>>>> - @StaplerPath(altName) annotated methods are only allowed if >>>>> targeting a new version Stapler that adds support for that (unless the >>>>> name >>>>> is the inferred name) >>>>> - @StaplerHEAD / @StaplerGET / @StaplerPOST / @StaplerPUT / >>>>> @StaplerPATCH / @StaplerDELETE annotated methods must be public >>>>> and match one of the allowed action method signatures. The method name >>>>> must >>>>> match the convention for action method inference (subject to >>>>> @WebMethod(name="...") overriding when *compiling against older >>>>> versions of Stapler* - but it would be an error to use @WebMethod >>>>> if compiling against newer versions of Stapler because we can move to >>>>> the >>>>> new annotations in those cases) >>>>> - If the class is @StaplerObject annotated we can do additional >>>>> checks for overlapping facets, for example where an action method or a >>>>> getter will hide a facet (it's ok to hide the facet for non HEAD/GET >>>>> requests though) >>>>> >>>>> There are probably additional checks that we can add as we figure them >>>>> out. >>>>> >>>>> >>>>> Now there are some open questions: >>>>> >>>>> - Does it make sense to start all these annotations with Stapler? >>>>> My initial stab at this was to use @Staple in place of @StaplerPath >>>>> and to re-use the @GET / @POST / @PUT / @DELETE verbs but >>>>> mandating the @Staple annotation so that you would have: >>>>> >>>>> @Staple @POST >>>>> >>>>> public HttpResponse doActivate(StaplerRequest req) { ... } >>>>> >>>>> >>>>> rather than >>>>> >>>>> @StaplerPOST >>>>> >>>>> public HttpResponse doActivate(StaplerRequest req) { ... } >>>>> >>>>> >>>>> One of the issues I found with that is class conflicts when >>>>> back-porting the @GET / @POST / etc annotations, so we could only use >>>>> those if we were prepared to mark 2.7 as the oldest core that could use >>>>> the >>>>> annotations, whereas we can make new annotations in a separate package >>>>> available as far back as Stapler 1.180 == Jenkins 1.455 (or worst case >>>>> Stapler 1.237 == Jenkins 1.651) >>>>> >>>>> Now we could introduce new annotations without the @Stapler prefix, >>>>> but then your code complete would confuse you between >>>>> @org.kohsuke.stapler.annotations.GET and >>>>> @org.kohsuke.stapler.verbs.GET plus we still need something to give a >>>>> hint that the annotation is implying Stapler's involvement (at least from >>>>> my perspective) >>>>> >>>>> >>>>> - I had thought about saying that the class level @StaplerObject >>>>> should be enough of a flag that you should expect the other >>>>> annotations... >>>>> but the class annotation will be inherited, so you may not see it >>>>> until you >>>>> look at the super-class... and @Path will conflict with >>>>> java.nio.file.Path... that might work with @Staple (and @Staples >>>>> as the container) but while cute they do not scream to the new >>>>> developers >>>>> that these are involved in url binding. >>>>> >>>>> NOTE: In this context I see this change as being entirely opt-in. >>>>> Plugin developers should not have to go adding the annotations - though we >>>>> would probably apply them in Jenkins core to assist new developers. >>>>> >>>>> >>>>> - If we go with the Stapler prefix, would it make sense to >>>>> consolidate all the annotations with that prefix? >>>>> >>>>> - I am not convinced for the case of @ExportedBean and @Exported, >>>>> these reflect a different cross-cutting concern and as such the >>>>> pair seem >>>>> named well from my PoV >>>>> - The @JavaScriptMethod / @WithWellKnownURL annotations might >>>>> be worthy of consolidation if we can determine better names. >>>>> - The parameter binding annotations such as @AncestorInPath, >>>>> @Header, @QueryParameter and @InjectedParameter I think are >>>>> fine where they are as they will only be on an action method which >>>>> already >>>>> has the @StaplerPOST etc indicator. >>>>> - The meta-annotation @InterceptorAnnotation should not be >>>>> consolidated, in any case we could not consolidate it and retain >>>>> usage >>>>> against older cores >>>>> - The other annotations are likely rarely used, but if we can >>>>> find good names and people think they are generically useful rather >>>>> than >>>>> single use-case hacks added into the stapler API, I think >>>>> consolidation >>>>> might make the features more widely used. >>>>> >>>>> So, over to you the community of Jenkins developers: >>>>> >>>>> 1. Would this change have helped you get started quicker? >>>>> 2. Would this change help you even now? >>>>> 3. How ugly are my proposed annotation names? Can you provide a >>>>> less ugly scheme of names? >>>>> 4. Anything else? >>>>> >>>>> Stephen >>>>> >>>>> -- >>>>> You received this message because you are subscribed to the Google >>>>> Groups "Jenkins Developers" group. >>>>> To unsubscribe from this group and stop receiving emails from it, send >>>>> an email to [email protected]. >>>>> To view this discussion on the web visit >>>>> https://groups.google.com/d/msgid/jenkinsci-dev/CA%2BnPnMz-m >>>>> 49TK7Em%2BxBNb%2BV98dBCz9CrrPXg3uW6%2B_x3KX5gOQ%40mail.gmail.com >>>>> <https://groups.google.com/d/msgid/jenkinsci-dev/CA%2BnPnMz-m49TK7Em%2BxBNb%2BV98dBCz9CrrPXg3uW6%2B_x3KX5gOQ%40mail.gmail.com?utm_medium=email&utm_source=footer> >>>>> . >>>>> For more options, visit https://groups.google.com/d/optout. >>>>> >>>> -- >>>> Kohsuke Kawaguchi >>>> >>>> -- >>>> You received this message because you are subscribed to the Google >>>> Groups "Jenkins Developers" group. >>>> To unsubscribe from this group and stop receiving emails from it, send >>>> an email to [email protected]. >>>> To view this discussion on the web visit https://groups.google.com/d/ms >>>> gid/jenkinsci-dev/CAN4CQ4wDS34DE-PQVK6tsCQD5AXWcxCMEVkCgh5MN >>>> ymRNVcLPw%40mail.gmail.com >>>> <https://groups.google.com/d/msgid/jenkinsci-dev/CAN4CQ4wDS34DE-PQVK6tsCQD5AXWcxCMEVkCgh5MNymRNVcLPw%40mail.gmail.com?utm_medium=email&utm_source=footer> >>>> . >>>> >>>> For more options, visit https://groups.google.com/d/optout. >>>> >>> >>> -- >>> You received this message because you are subscribed to the Google >>> Groups "Jenkins Developers" group. >>> To unsubscribe from this group and stop receiving emails from it, send >>> an email to [email protected]. >>> To view this discussion on the web visit https://groups.google.com/d/ms >>> gid/jenkinsci-dev/CA%2BnPnMwio9PeC3uhC_i4XqQ_7OY2dN2Hx34e6hS >>> mHVAeG7RiRA%40mail.gmail.com >>> <https://groups.google.com/d/msgid/jenkinsci-dev/CA%2BnPnMwio9PeC3uhC_i4XqQ_7OY2dN2Hx34e6hSmHVAeG7RiRA%40mail.gmail.com?utm_medium=email&utm_source=footer> >>> . >>> >>> For more options, visit https://groups.google.com/d/optout. >>> >> >> >> >> -- >> Robert Sandell >> *Software Engineer* >> *CloudBees Inc.* >> >> -- >> You received this message because you are subscribed to the Google Groups >> "Jenkins Developers" group. >> To unsubscribe from this group and stop receiving emails from it, send an >> email to [email protected]. >> To view this discussion on the web visit https://groups.google.com/d/ms >> gid/jenkinsci-dev/CALzHZS10%2BzMD58BLJBiksxY7ch7sDQ7rPQEN9y- >> SJACGhREoTA%40mail.gmail.com >> <https://groups.google.com/d/msgid/jenkinsci-dev/CALzHZS10%2BzMD58BLJBiksxY7ch7sDQ7rPQEN9y-SJACGhREoTA%40mail.gmail.com?utm_medium=email&utm_source=footer> >> . >> >> For more options, visit https://groups.google.com/d/optout. >> > > -- You received this message because you are subscribed to the Google Groups "Jenkins Developers" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion on the web visit https://groups.google.com/d/msgid/jenkinsci-dev/CA%2BnPnMzcfG4FX%3DH2tQNsL-Sptgj_%3DsRB9mNQ8kqGDeP3Tn4p0Q%40mail.gmail.com. For more options, visit https://groups.google.com/d/optout.
