Minor release for Bitbucket Server 5.10.0 now available

Just a quick post to alert you all to a new minor release of Bitbucket Server.

Bitbucket Server 5.10.0 Released
See the release notes for Bitbucket Server 5.10.0 for more information on this minor release

Released April 24, 2018

2 Likes

Bitbucket 5.10 has a new UI. We develop the Awesome Graphs app and encountered an issue with supporting both new and old UI.

We have problems with organizing resources (colors, styles) without duplicating them twice. Do you have some recommendations for simultaneous operation addon in the context “classic” (bitbucket before 5.10) and “modern” ui?

I showed the email digest options Tuesday night and yesterday at AUGs and it swayed 3 companies into “we need to upgrade right now” mode. One administrator said he was glad to get the heads-up at AUG, because developers at his company would be banging down his door for an upgrade if they found it first. Pretty exciting!

2 Likes

Hi @yorlov,

I’m not sure how you’re getting your styles now. The AUI components should be updated mostly for “free”, but I understand everything might need a tweak for unique contexts and bespoke components will need to be styled to match. I can think of four possible options for you to achieve this.

If you’re using the LESS support in Web Resources Manager, then you have access to LESS variables. For our own code, we put all our variables in a “global.less” file that we @import into every other file. This way our colors/spacings/etc are defined in one place. All these options depend on you having something like that (and now you’d have two version of that file).

One option is a build-time task that chooses which version of the file to use for different versions of the plugin. But I know that having two versions of the plugin has maintenance problems of its own.

Another option is to write a Web Resource Manager Transformer that checks the product version and transforms the content of each LESS file so that it @imports the right set of variables for that product version. The <transformer key="lessTransformer" /> can be called afterward to act on output from your transformer. I believe you’d have to apply this transformer to every LESS file that needs multiple styles (because LESS @imports use file paths directly, there’s no way to transform the import that I can think of.

The third option is to have 2 web-resources for every file that needs alternate styles. You can use a Web Resource Condition so that only one of them actually gets on the page, depending on product version. These web resources would contain one file that imports a given version of your variables and then imports the content that will use each set of variables. I’m told this is a pattern used by our Portfolio team. So the file in each resource would look something like:

@import "old-variables";
@import "my-code-using-variables";

and

@import "new-variables";
@import "my-code-using-variables";

The last option I can think of is potentially riskier. Our own LESS variables are not stable API and can change at any time. But if you’re happy with that risk, you might be able to use it to your advantage. In LESS, the last definition of a variable wins. So if you know there is a variable in new versions that isn’t in older versions, you can “underwrite” with a fallback. The code would look something like:

// @ak-color-N20 was added in 5.10, so you can set a value for use in versions before that.
@ak-color-N20: #badbad;
// @ak-color-N20 will get overwritten here in 5.10.
@import "webstatic:/static/global.less";

.my-thing {
    color: @ak-color-N20; /* custom or new value, depending on product version */
}

But note that this is not API and subject to change. The effect would be that if this variable goes away in 5.11, you’ll start showing old styles in 5.11 and have to find another workaround.

They all have pros and cons, but hopefully, one of those works for you!

Cheers,
Adam

3 Likes

Hi @yorlov,

When you say you are encountering an issue with supporting both the new and old UI, can you be more specific about what the issue is?

If the issue is “it looks different” between the older and newer versions… does that matter? Does the colour scheme make a tangible impact on your customer’s impressions or your net revenue? If not, does investing in papering over the differences between the two versions make financial sense?

While there are technical solutions, they all come with tradeoffs. I’m hoping you can explain why this problem is important before we get deeper in to the weeds of those tradeoffs.

Cheers,
Daz

1 Like

@aahmed, thanks for the comprehensive answer

@daz, our product provides charts. It is critically important that they look natively and consistently in accordance with Atlassian Design Guidelines

1 Like

Hi @aahmed,

I tried the solution with a transformer.

I have file graph-repository.less

@import "colors";

#graph {
  .info {
    background-color: @red;
  }
}

in directory resources/less there are files modern-colors.less, classic-colors.less and graph-repository.less

the following web-resource is defined

<web-resource key="graph-client-side">
    <transformation extension="less">
       	<transformer key="compatibility-less-transformer"/>
		<transformer key="less-transformer"/>
    </transformation>

    <resource type="download" name="graph-repository.css" location="less/graph-repository.less"/>

    <context>bitbucket.plugins.awesome.graphs</context>
</web-resource>

I expect that the content of the file graph-repository.less will get into my transformer (compatibility-less-transformer) at first where the string @import "colors"; would be replaced by @import "modern-colors"; or @import "classic-colors" depending on the conditions. After that, the result would get into less-transformer, would be converted into css and I would get correct colors on the page.

In a debugger, I see that the input that came to the less-transformer is a changed file

@import "modern-colors";

#graph {
  .info {
    background-color: @red;
  }
}

but further control is transferred to javascript and i catch this exception in logs

Stacktrace

2018-05-03 12:21:22,803 WARN [http-nio-7990-exec-5] admin @1VZ1PZDx741x3213x0 81dygo 192.168.0.3 “GET /plugins/servlet/graphs/activity/JETBRAINS/gradle-intellij-plugin HTTP/1.1” webresource error thrown in transformer during url generation for com.stiltsoft.stash.graphs:graph-client-resources
com.google.common.util.concurrent.UncheckedExecutionException: java.lang.IllegalStateException: java.io.IOException: /less/colors.less does not exist in plugin com.stiltsoft.stash.graphs
at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2203)
at com.google.common.cache.LocalCache.get(LocalCache.java:3937)
at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3941)
at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4824)
at com.google.common.cache.LocalCache$LocalLoadingCache.getUnchecked(LocalCache.java:4830)
at com.atlassian.plugins.less.CachingUriStateManager.collectUriState(CachingUriStateManager.java:104)
at com.atlassian.plugins.less.CachingUriStateManager.collectUriState(CachingUriStateManager.java:118)
at com.atlassian.plugins.less.CachingUriStateManager.getState(CachingUriStateManager.java:70)
at com.atlassian.plugins.less.LessTransformerUrlBuilder.addToUrl(LessTransformerUrlBuilder.java:37)
at com.atlassian.plugin.webresource.impl.UrlBuildingStrategy$NormalStrategy.addToUrl(UrlBuildingStrategy.java:44)
at com.atlassian.plugin.webresource.WebResourceTransformation.addTransformParameters(WebResourceTransformation.java:110)
at com.atlassian.plugin.webresource.impl.CachedTransformers.addToUrlSafely(CachedTransformers.java:43)
at com.atlassian.plugin.webresource.impl.helpers.UrlGenerationHelpers.encodeStateInUrlIfSupported(UrlGenerationHelpers.java:840)
at com.atlassian.plugin.webresource.impl.helpers.UrlGenerationHelpers.collectUrlStateAndBuildResourceUrls(UrlGenerationHelpers.java:390)
at com.atlassian.plugin.webresource.impl.helpers.UrlGenerationHelpers.resolve(UrlGenerationHelpers.java:227)
at com.atlassian.plugin.webresource.assembler.DefaultWebResourceAssembler$1.resolve(DefaultWebResourceAssembler.java:86)
at com.atlassian.plugin.webresource.assembler.DefaultWebResourceAssembler$1.peek(DefaultWebResourceAssembler.java:82)
at com.atlassian.plugin.webresource.WebResourceManagerImpl.writeIncludedResources(WebResourceManagerImpl.java:227)
at com.atlassian.plugin.webresource.WebResourceManagerImpl.writeIncludedResources(WebResourceManagerImpl.java:194)
at com.atlassian.plugin.webresource.WebResourceManagerImpl.includeResources(WebResourceManagerImpl.java:129)
at com.atlassian.plugin.webresource.WebResourceManagerImpl.includeResources(WebResourceManagerImpl.java:114)
at com.atlassian.stash.internal.plugin.ProfiledWebResourceManager.includeResources(ProfiledWebResourceManager.java:27)
at com.atlassian.plugin.util.ContextClassLoaderSettingInvocationHandler.invoke(ContextClassLoaderSettingInvocationHandler.java:26)
at org.eclipse.gemini.blueprint.service.importer.support.internal.aop.ServiceInvoker.doInvoke(ServiceInvoker.java:56)
at org.eclipse.gemini.blueprint.service.importer.support.internal.aop.ServiceInvoker.invoke(ServiceInvoker.java:60)
at org.eclipse.gemini.blueprint.service.util.internal.aop.ServiceTCCLInterceptor.invokeUnprivileged(ServiceTCCLInterceptor.java:70)
at org.eclipse.gemini.blueprint.service.util.internal.aop.ServiceTCCLInterceptor.invoke(ServiceTCCLInterceptor.java:53)
at org.eclipse.gemini.blueprint.service.importer.support.LocalBundleContextAdvice.invoke(LocalBundleContextAdvice.java:57)
at com.atlassian.soy.impl.functions.IncludeResourcesFunction.computeForJava(IncludeResourcesFunction.java:33)
at com.atlassian.soy.impl.functions.IncludeResourcesFunction.computeForJava(IncludeResourcesFunction.java:18)
at com.google.template.soy.sharedpasses.render.EvalVisitor.computeFunctionHelper(EvalVisitor.java:670)
at com.google.template.soy.sharedpasses.render.EvalVisitor.visitFunctionNode(EvalVisitor.java:653)
at com.google.template.soy.sharedpasses.render.EvalVisitor.visitFunctionNode(EvalVisitor.java:87)
at com.google.template.soy.exprtree.AbstractReturningExprNodeVisitor.visit(AbstractReturningExprNodeVisitor.java:118)
at com.google.template.soy.sharedpasses.render.EvalVisitor.visitExprRootNode(EvalVisitor.java:148)
at com.google.template.soy.sharedpasses.render.EvalVisitor.visitExprRootNode(EvalVisitor.java:87)
at com.google.template.soy.exprtree.AbstractReturningExprNodeVisitor.visit(AbstractReturningExprNodeVisitor.java:81)
at com.google.template.soy.exprtree.AbstractReturningExprNodeVisitor.visit(AbstractReturningExprNodeVisitor.java:73)
at com.google.template.soy.basetree.AbstractReturningNodeVisitor.exec(AbstractReturningNodeVisitor.java:43)
at com.google.template.soy.sharedpasses.render.RenderVisitor.eval(RenderVisitor.java:739)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitPrintNode(RenderVisitor.java:248)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:87)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.visitChildren(AbstractNodeVisitor.java:59)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitChildren(AbstractSoyNodeVisitor.java:129)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitBlockHelper(RenderVisitor.java:702)
at com.google.template.soy.sharedpasses.render.RenderVisitor.renderBlock(RenderVisitor.java:716)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallNodeHelper(RenderVisitor.java:564)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallBasicNode(RenderVisitor.java:457)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:110)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.visitChildren(AbstractNodeVisitor.java:59)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitChildren(AbstractSoyNodeVisitor.java:129)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitBlockHelper(RenderVisitor.java:702)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitTemplateNode(RenderVisitor.java:220)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitTemplateBasicNode(AbstractSoyNodeVisitor.java:160)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:66)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.exec(AbstractNodeVisitor.java:40)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallNodeHelper(RenderVisitor.java:590)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallBasicNode(RenderVisitor.java:457)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:110)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.visitChildren(AbstractNodeVisitor.java:59)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitChildren(AbstractSoyNodeVisitor.java:129)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitBlockHelper(RenderVisitor.java:702)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitTemplateNode(RenderVisitor.java:220)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitTemplateBasicNode(AbstractSoyNodeVisitor.java:160)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:66)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.exec(AbstractNodeVisitor.java:40)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallNodeHelper(RenderVisitor.java:590)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallBasicNode(RenderVisitor.java:457)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:110)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.visitChildren(AbstractNodeVisitor.java:59)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitChildren(AbstractSoyNodeVisitor.java:129)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitBlockHelper(RenderVisitor.java:702)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitTemplateNode(RenderVisitor.java:220)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitTemplateBasicNode(AbstractSoyNodeVisitor.java:160)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:66)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.exec(AbstractNodeVisitor.java:40)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallNodeHelper(RenderVisitor.java:590)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallBasicNode(RenderVisitor.java:457)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:110)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.visitChildren(AbstractNodeVisitor.java:59)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitChildren(AbstractSoyNodeVisitor.java:129)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitBlockHelper(RenderVisitor.java:702)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitTemplateNode(RenderVisitor.java:220)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitTemplateBasicNode(AbstractSoyNodeVisitor.java:160)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:66)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.exec(AbstractNodeVisitor.java:40)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallNodeHelper(RenderVisitor.java:590)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallBasicNode(RenderVisitor.java:457)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:110)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.visitChildren(AbstractNodeVisitor.java:59)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitChildren(AbstractSoyNodeVisitor.java:129)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitBlockHelper(RenderVisitor.java:698)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitTemplateNode(RenderVisitor.java:220)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitTemplateBasicNode(AbstractSoyNodeVisitor.java:160)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:66)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.exec(AbstractNodeVisitor.java:40)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallNodeHelper(RenderVisitor.java:590)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitCallBasicNode(RenderVisitor.java:457)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:110)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.visitChildren(AbstractNodeVisitor.java:59)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitChildren(AbstractSoyNodeVisitor.java:129)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitBlockHelper(RenderVisitor.java:702)
at com.google.template.soy.sharedpasses.render.RenderVisitor.visitTemplateNode(RenderVisitor.java:220)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visitTemplateBasicNode(AbstractSoyNodeVisitor.java:160)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:66)
at com.google.template.soy.soytree.AbstractSoyNodeVisitor.visit(AbstractSoyNodeVisitor.java:56)
at com.google.template.soy.basetree.AbstractNodeVisitor.exec(AbstractNodeVisitor.java:40)
at com.google.template.soy.tofu.internal.BaseTofu.renderMainHelper(BaseTofu.java:366)
at com.google.template.soy.tofu.internal.BaseTofu.renderMain(BaseTofu.java:322)
at com.google.template.soy.tofu.internal.BaseTofu.access$100(BaseTofu.java:66)
at com.google.template.soy.tofu.internal.BaseTofu$RendererImpl.render(BaseTofu.java:476)
at com.atlassian.soy.impl.DefaultSoyManager.render(DefaultSoyManager.java:154)
at com.atlassian.soy.impl.DefaultSoyTemplateRenderer.render(DefaultSoyTemplateRenderer.java:45)
at org.eclipse.gemini.blueprint.service.importer.support.internal.aop.ServiceInvoker.doInvoke(ServiceInvoker.java:56)
at org.eclipse.gemini.blueprint.service.importer.support.internal.aop.ServiceInvoker.invoke(ServiceInvoker.java:60)
at org.eclipse.gemini.blueprint.service.util.internal.aop.ServiceTCCLInterceptor.invokeUnprivileged(ServiceTCCLInterceptor.java:70)
at org.eclipse.gemini.blueprint.service.util.internal.aop.ServiceTCCLInterceptor.invoke(ServiceTCCLInterceptor.java:53)
at org.eclipse.gemini.blueprint.service.importer.support.LocalBundleContextAdvice.invoke(LocalBundleContextAdvice.java:57)
at com.stiltsoft.bitbucket.graphs.servlet.AbstractGraphsServlet.doGet(AbstractGraphsServlet.java:79)
at com.stiltsoft.bitbucket.graphs.servlet.RepositoryGraphsServlet.doGet(RepositoryGraphsServlet.java:60)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.analytics.client.filter.UniversalAnalyticsFilter.doFilter(UniversalAnalyticsFilter.java:92)
at com.atlassian.analytics.client.filter.AbstractHttpFilter.doFilter(AbstractHttpFilter.java:39)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.opensymphony.sitemesh.webapp.SiteMeshFilter.obtainContent(SiteMeshFilter.java:181)
at com.opensymphony.sitemesh.webapp.SiteMeshFilter.doFilter(SiteMeshFilter.java:85)
at com.atlassian.plugin.connect.plugin.auth.scope.ApiScopingFilter.doFilter(ApiScopingFilter.java:81)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.stash.internal.spring.security.StashAuthenticationFilter.doFilter(StashAuthenticationFilter.java:85)
at com.atlassian.stash.internal.web.auth.BeforeLoginPluginAuthenticationFilter.doInsideSpringSecurityChain(BeforeLoginPluginAuthenticationFilter.java:112)
at com.atlassian.stash.internal.web.auth.BeforeLoginPluginAuthenticationFilter.doFilter(BeforeLoginPluginAuthenticationFilter.java:75)
at com.atlassian.security.auth.trustedapps.filter.TrustedApplicationsFilter.doFilter(TrustedApplicationsFilter.java:94)
at com.atlassian.oauth.serviceprovider.internal.servlet.OAuthFilter.doFilter(OAuthFilter.java:67)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.plugin.connect.plugin.auth.oauth2.DefaultSalAuthenticationFilter.doFilter(DefaultSalAuthenticationFilter.java:69)
at com.atlassian.plugin.connect.plugin.auth.user.ThreeLeggedAuthFilter.doFilter(ThreeLeggedAuthFilter.java:109)
at com.atlassian.jwt.internal.servlet.JwtAuthFilter.doFilter(JwtAuthFilter.java:32)
at com.atlassian.analytics.client.filter.DefaultAnalyticsFilter.doFilter(DefaultAnalyticsFilter.java:38)
at com.atlassian.analytics.client.filter.AbstractHttpFilter.doFilter(AbstractHttpFilter.java:39)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.stash.internal.web.auth.BeforeLoginPluginAuthenticationFilter.doBeforeBeforeLoginFilters(BeforeLoginPluginAuthenticationFilter.java:90)
at com.atlassian.stash.internal.web.auth.BeforeLoginPluginAuthenticationFilter.doFilter(BeforeLoginPluginAuthenticationFilter.java:73)
at com.atlassian.stash.internal.request.DefaultRequestManager.doAsRequest(DefaultRequestManager.java:89)
at com.atlassian.stash.internal.hazelcast.ConfigurableWebFilter.doFilter(ConfigurableWebFilter.java:38)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.lang.Thread.run(Thread.java:745)
… 280 frames trimmed
Caused by: java.lang.IllegalStateException: java.io.IOException: /less/colors.less does not exist in plugin com.stiltsoft.stash.graphs
at com.atlassian.plugins.less.UriDependencyCollector.getDependencies(UriDependencyCollector.java:52)
at com.atlassian.plugins.less.CachingUriStateManager.computeUriInfo(CachingUriStateManager.java:127)
at com.atlassian.plugins.less.CachingUriStateManager.access$000(CachingUriStateManager.java:31)
at com.atlassian.plugins.less.CachingUriStateManager$1.load(CachingUriStateManager.java:54)
at com.atlassian.plugins.less.CachingUriStateManager$1.load(CachingUriStateManager.java:51)
at com.google.common.cache.LocalCache$LoadingValueReference.loadFuture(LocalCache.java:3527)
at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2319)
at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2282)
at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2197)
… 162 common frames omitted
Caused by: java.io.IOException: /less/colors.less does not exist in plugin com.stiltsoft.stash.graphs
at com.atlassian.plugins.less.PluginUriResolver.open(PluginUriResolver.java:120)
at com.atlassian.plugins.less.UriDependencyCollector.getDependencies(UriDependencyCollector.java:45)
… 170 common frames omitted
2018-05-03 12:21:23,191 WARN [http-nio-7990-exec-3] @1VZ1PZDx741x3214x0 81dygo 192.168.0.3 “GET /s/d1f69e5cbf525b8aafd9bbd0be367847-CDN/-1309557296/76ff11a/931/d5fa8e99652796385b358e397f6ee515/_/download/contextbatch/css/bitbucket.plugins.awesome.graphs,bitbucket.layout.branch,bitbucket.layout.repository,bitbucket.layout.base,atl.general,bitbucket.layout.entity,-_super/batch.css HTTP/1.1” webresource error in Content.writeTo for com.stiltsoft.stash.graphs:graph-client-side/graph-repository.css
com.atlassian.lesscss.UnresolvableImportException: Cannot find …/…/less/colors.less
at com.atlassian.lesscss.RhinoLessCompiler.newLessException(RhinoLessCompiler.java:88)
at com.atlassian.lesscss.RhinoLessCompiler.compile(RhinoLessCompiler.java:72)
at com.atlassian.plugins.less.LessResource.transform(LessResource.java:42)
at com.atlassian.plugin.webresource.transformer.CharSequenceDownloadableResource$1.apply(CharSequenceDownloadableResource.java:44)
at com.atlassian.plugin.webresource.transformer.CharSequenceDownloadableResource$1.apply(CharSequenceDownloadableResource.java:42)
at com.atlassian.plugin.webresource.transformer.TransformerUtils.transformAndStreamResource(TransformerUtils.java:40)
at com.atlassian.plugin.webresource.transformer.CharSequenceDownloadableResource.streamResource(CharSequenceDownloadableResource.java:42)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$5.writeTo(ResourceServingHelpers.java:351)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$9.writeTo(ResourceServingHelpers.java:478)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$8.writeTo(ResourceServingHelpers.java:466)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$4.streamResource(ResourceServingHelpers.java:332)
at com.atlassian.plugin.webresource.transformer.TransformerUtils.transformAndStreamResource(TransformerUtils.java:38)
at com.atlassian.plugin.webresource.transformer.CharSequenceDownloadableResource.streamResource(CharSequenceDownloadableResource.java:42)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$5.writeTo(ResourceServingHelpers.java:351)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$9.writeTo(ResourceServingHelpers.java:478)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$7.writeTo(ResourceServingHelpers.java:379)
at com.atlassian.plugin.webresource.impl.helpers.ResourceServingHelpers$1.writeTo(ResourceServingHelpers.java:126)
at com.atlassian.plugin.webresource.impl.http.Controller.lambda$sendCachedInProduction$0(Controller.java:319)
at com.atlassian.plugin.cache.filecache.impl.OneStreamCache.streamToCache(OneStreamCache.java:83)
at com.atlassian.plugin.cache.filecache.impl.OneStreamCache$1.apply(OneStreamCache.java:40)
at com.atlassian.plugin.cache.filecache.impl.StreamsCache.doEnter(StreamsCache.java:74)
at com.atlassian.plugin.cache.filecache.impl.OneStreamCache.stream(OneStreamCache.java:36)
at com.atlassian.plugin.cache.filecache.impl.FileCacheImpl.cache(FileCacheImpl.java:87)
at com.atlassian.plugin.webresource.impl.http.Controller.sendCachedInProduction(Controller.java:319)
at com.atlassian.plugin.webresource.impl.http.Controller.sendCached(Controller.java:282)
at com.atlassian.plugin.webresource.impl.http.Controller.serveResources(Controller.java:222)
at com.atlassian.plugin.webresource.impl.http.Controller.serveBatch(Controller.java:84)
at com.atlassian.plugin.webresource.impl.http.Router$5.apply(Router.java:78)
at com.atlassian.plugin.webresource.impl.http.Router$5.apply(Router.java:71)
at com.atlassian.plugin.webresource.impl.support.http.BaseRouter.callHandler(BaseRouter.java:169)
at com.atlassian.plugin.webresource.impl.support.http.BaseRouter.dispatch(BaseRouter.java:144)
at com.atlassian.plugin.webresource.servlet.PluginResourceDownload.serveFile(PluginResourceDownload.java:65)
at com.atlassian.stash.internal.plugin.FileServerServlet.access$000(FileServerServlet.java:24)
at com.atlassian.stash.internal.plugin.FileServerServlet$1.perform(FileServerServlet.java:41)
at com.atlassian.stash.internal.locale.LocaleUtils.withoutLocale(LocaleUtils.java:25)
at com.atlassian.stash.internal.plugin.FileServerServlet.handleRequest(FileServerServlet.java:38)
at org.springframework.web.context.support.HttpRequestHandlerServlet.service(HttpRequestHandlerServlet.java:67)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.applinks.core.rest.context.ContextFilter.doFilter(ContextFilter.java:24)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.security.auth.trustedapps.filter.TrustedApplicationsFilter.doFilter(TrustedApplicationsFilter.java:94)
at com.atlassian.oauth.serviceprovider.internal.servlet.OAuthFilter.doFilter(OAuthFilter.java:67)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.security.auth.trustedapps.filter.TrustedApplicationsFilter.doFilter(TrustedApplicationsFilter.java:94)
at com.atlassian.oauth.serviceprovider.internal.servlet.OAuthFilter.doFilter(OAuthFilter.java:67)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.plugin.connect.plugin.auth.oauth2.DefaultSalAuthenticationFilter.doFilter(DefaultSalAuthenticationFilter.java:69)
at com.atlassian.plugin.connect.plugin.auth.user.ThreeLeggedAuthFilter.doFilter(ThreeLeggedAuthFilter.java:109)
at com.atlassian.jwt.internal.servlet.JwtAuthFilter.doFilter(JwtAuthFilter.java:32)
at com.atlassian.analytics.client.filter.DefaultAnalyticsFilter.doFilter(DefaultAnalyticsFilter.java:38)
at com.atlassian.analytics.client.filter.AbstractHttpFilter.doFilter(AbstractHttpFilter.java:39)
at com.atlassian.stash.internal.spring.lifecycle.LifecycleJohnsonServletFilterModuleContainerFilter.doFilter(LifecycleJohnsonServletFilterModuleContainerFilter.java:42)
at com.atlassian.stash.internal.web.auth.BeforeLoginPluginAuthenticationFilter.doBeforeBeforeLoginFilters(BeforeLoginPluginAuthenticationFilter.java:90)
at com.atlassian.stash.internal.web.auth.BeforeLoginPluginAuthenticationFilter.doFilter(BeforeLoginPluginAuthenticationFilter.java:73)
at com.atlassian.stash.internal.request.DefaultRequestManager.doAsRequest(DefaultRequestManager.java:89)
at com.atlassian.stash.internal.hazelcast.ConfigurableWebFilter.doFilter(ConfigurableWebFilter.java:38)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.lang.Thread.run(Thread.java:745)
… 236 frames trimmed
Caused by: org.mozilla.javascript.JavaScriptException: [object Object] (/js/less/less-patches.js#253)
at org.mozilla.javascript.gen._js_less_less_patches_js_2._c_runLessRun_29(/js/less/less-patches.js:253)
at org.mozilla.javascript.gen._js_less_less_patches_js_2.call(/js/less/less-patches.js)
at org.mozilla.javascript.ScriptRuntime.applyOrCall(ScriptRuntime.java:2429)
at org.mozilla.javascript.BaseFunction.execIdCall(BaseFunction.java:269)
at org.mozilla.javascript.IdFunctionObject.call(IdFunctionObject.java:97)
at org.mozilla.javascript.optimizer.OptRuntime.call2(OptRuntime.java:42)
at org.mozilla.javascript.gen._js_less_less_patches_js_2._c_anonymous_28(/js/less/less-patches.js:219)
at org.mozilla.javascript.gen._js_less_less_patches_js_2.call(/js/less/less-patches.js)
at org.mozilla.javascript.ContextFactory.doTopCall(ContextFactory.java:394)
at org.mozilla.javascript.ScriptRuntime.doTopCall(ScriptRuntime.java:3090)
at org.mozilla.javascript.gen._js_less_less_patches_js_2.call(/js/less/less-patches.js)
at com.atlassian.lesscss.RhinoLessCompiler.compile(RhinoLessCompiler.java:69)
… 62 common frames omitted

My transformer code looks like this at the moment

public class ADG3CompatibilityLessTransformer implements WebResourceTransformerFactory {

    private static Pattern SEPARATOR = Pattern.compile("\n");

    @Override
    public TransformerUrlBuilder makeUrlBuilder(TransformerParameters parameters) {
        return builder -> {};
    }

    @Override
    public UrlReadingWebResourceTransformer makeResourceTransformer(TransformerParameters parameters) {
        return (transformableResource, params) -> new CharSequenceDownloadableResource(transformableResource.nextResource()) {
            @Override
            protected CharSequence transform(CharSequence original) {
                return SEPARATOR.splitAsStream(original)
                        .map(line -> {
                            if (line.startsWith("@import") && line.endsWith("colors\";")) {
                                return line.replace("colors", "modern-colors");
                            }
                            return line;
                        })
                        .collect(joining(SEPARATOR.pattern()));
            }
        };
    }
}

Probably I should implement additional logic in TransformerUrlBuilder but I am not sure.
What can you advise in this case?

I’m sorry to say I hadn’t considered this issue. The app is trying to build a URL it can cache forever, and that code isn’t chained like the transformation itself is (which seems like a shortcoming of the design) so you can’t inject yourself in front of the LESS transformer for that step in the same way you can for the actual transformation. I apologize for missing that and now I’m worried about any future gotchas that might turn this option into a wild goose chase.

You might be able to work around this still if you add an empty colors.less file to your plugin? The UrlBuilder is for determining the versions of plugins you depend on so the LESS resource can be cached forever and the URL changed when a plugin is updated. If you’re importing external URLs in the modern/classic colors files, you could import both modern and classic files in colors.less so that cacheable URL is built with the right dependencies in mind.

If that doesn’t work, you might try implementing a UriResolver (based heavily on com.atlassian.plugins.less.PluginUriResolver, but which only supports() your one colors URL which could have a custom schema like stiltsoft:// and will open() the modern or classic file depending on product version). This would probably mean you get rid of the custom transformer altogether.

If you need more detailed/private help on this one, feel free to email me at aahmed at atlassian and I’ll see what I can do. Apologies again for the potential dead-end.

I can think of two other, slightly less elegant ways you might solve this.

First option

This strategy would exploit LESS’ cascade and web-resource conditions. During the parsing of a LESS file, any code defined in one file will be available to all subsequent files, even if they don’t have an explicit dependency. So, you could create two LESS files like so:

# old-styles.less
@import 'legacy-colors';
@import './the/actual/feature-code';

# new-styles.less
@import 'modern-colors';
@import './the/actual/feature-code';

The actual feature code LESS file wouldn’t import the variables; rather, it would expect them to have been previously defined.

You would then create two web-resources – one for the old versions of Bitbucket, a second for the new ones. The web-resource condition would check the version of Bitbucket it’s installed in: if it’s one with the new ADG design, the modern web-resource will output its code; otherwise, the legacy one will. You can see this conditional web-resource technique in use in this tutorial: https://developer.atlassian.com/platform/marketplace/ommitting-web-resources-for-unlicensed-server-apps/

Second option

You could implement a “bridging” pattern in Java, whose purpose is to choose between partial atlassian-plugin.xml plugin definitions when your plugin is installed. This avoids needing the web-resource condition, at the added complexity of needing to dynamically compose the web-resource definitions that your plugin installs and enables.

There’s a utility project called “pocketknife-dynamic-modules” that enables this kind of behaviour: Bitbucket. If I recall, this is in active use by Jira ServiceDesk as well as Jira Software. If you have access to either codebase, you should find additional example usages within those by searching for the project’s packages.

I hope that helps you get a bit closer to serving different colours to different versions.

Cheers,
Daz

I have the web-item

<web-item key="user-contributions-tab" name="User Contributions Tab" section="bitbucket.user.profile.secondary.tabs" weight="20">
	<label>Contributions</label>
	<link>${navBuilder.pluginServlets().path('user-contributions', $profileUser.slug).buildRelNoContext()}</link>
	<tooltip key="graphs.user.contributions.button.tooltip.message"/>
</web-item>

In bitbucket 5.9 a web-item look like

In bitbucket 5.10 a web-item look like

What kind of changes are worth making in my web-item to remove big space between items in bitbucket 5.10? Or is this space a bitbucket ui bug?

Hey @yorlov, that’s a UI bug to do with how Bitbucket uses AUI’s tabs component. The Bitbucket team brought it to my attention in [AUI-4762] - Ecosystem Jira. To my knowledge, the issue was fixed in Bitbucket a month or so ago. I do not know when their fix is intended to ship.

@aahmed, @daz
Thank you for your help. Great job!