<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Vyacheslav Rusakov's Blog]]></title><description><![CDATA[Vyacheslav Rusakov's Blog]]></description><link>https://blog.vyarus.ru</link><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 11:32:20 GMT</lastBuildDate><atom:link href="https://blog.vyarus.ru/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Gradle configuration cache by example. Part 2:  problems]]></title><description><![CDATA[In the first part we have seen how configuration cache is working. Now I will briefly show what problems configuration cache brings to plugin authors with possible solutions.
Searching for problems
There are two types of problems you may face:

On fi...]]></description><link>https://blog.vyarus.ru/gradle-configuration-cache-by-example-part-2-problems</link><guid isPermaLink="true">https://blog.vyarus.ru/gradle-configuration-cache-by-example-part-2-problems</guid><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Mon, 08 Sep 2025 09:13:46 GMT</pubDate><content:encoded><![CDATA[<p>In the first part we have seen how configuration cache is working. Now I will briefly show what problems configuration cache brings to plugin authors with possible solutions.</p>
<h2 id="heading-searching-for-problems">Searching for problems</h2>
<p>There are two types of problems you may face:</p>
<ul>
<li><p>On first run (with cache enabled) gradle could indicate problems with configuration state serialization . Not always obvious to track, but, ususallly, quite easy to fix.</p>
</li>
<li><p>On second run (from cache) there might be problems because of <em>incorrect assumptions</em>. For example, you rely on build service data (filled at configuration time), which is not preserved when running from cache (build service state not serialized).</p>
</li>
</ul>
<p>We will see both types of problems below.</p>
<h2 id="heading-access-project-at-runtime">Access project at runtime</h2>
<p>The simplest and the most popular problem is using <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:disallowed_types">not allowed objects</a> at runtime. Most likely, you’ll face <code>project</code> usage in task:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail1Task</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">DefaultTask</span> </span>{

    <span class="hljs-meta">@TaskAction</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[run] Task executed for project: "</span> + getProject().getName());
    }
}
</code></pre>
<p>And a very simple plugin:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail1Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
          project.getTasks().register(<span class="hljs-string">"fail1Task"</span>, Fail1Task.class);
    }
}
</code></pre>
<p>It will fail to run <code>fail1Task --configuration-cache --configuration-cache-problems=warn</code></p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: fail1Task

&gt; Task :fail1Task
[run] Task executed for project: junit14814233387960710613

1 problem was found storing the configuration cache.
- Task `:fail1Task` of type `ru.vyarus.gradle.plugin.fails.fail1.Fail1Task`: invocation of 'Task.project' at execution time is unsupported.
  See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution

See the complete report at file:///tmp/junit14814233387960710613/build/reports/configuration-cache/2n0qomukpcgdka6np2vpry01g/a4zpdprgh0056ayfdj5dwvtkk/configuration-cache-report.html

[Incubating] Problems report is available at: file:///tmp/junit14814233387960710613/build/reports/problems/problems-report.html

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored with 1 problem.
</code></pre>
<p><strong>Note</strong>: <code>configuration-cache-problems=warn</code> is used to prevent build failure for simpler output (without stacktraces) and to show some additional side effects (in later cases).</p>
<p>To fix this, project object access must be moved to configuration phase. It could be done by storing required data in task property in the constructor (called at configuration time):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail1FixTask</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">DefaultTask</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String projectName;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Fail1FixTask</span><span class="hljs-params">()</span> </span>{
        projectName = getProject().getName();
    }

    <span class="hljs-meta">@TaskAction</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[run] Task executed for project: "</span> + projectName);
    }
}
</code></pre>
<p>Or, with provider (if state resolution must be delayed):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail1Fix2Task</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">DefaultTask</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Provider&lt;String&gt; projectName;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Fail1Fix2Task</span><span class="hljs-params">()</span> </span>{
        projectName = getProject().provider(() -&gt; getProject().getName());
    }

    <span class="hljs-meta">@TaskAction</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[run] Task executed for project: "</span> + projectName.get());
    }
}
</code></pre>
<p><strong>IMPORTANT</strong>: Cases with other gradle objects are not shown becuase they are clearly <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:use_project_during_execution">described in the docs</a>.</p>
<h2 id="heading-project-usage-in-plugin">Project usage in plugin</h2>
<p>Project (or other <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:disallowed_types">not allowed objects</a>) usage at plugin’s run block also fails. The plugin blow access project in the task’s <code>doLast</code> block (executed at runtime):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail2Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Project name: "</span> + project.getName());

        project.getTasks().register(<span class="hljs-string">"fail2Task"</span>, task -&gt;  {
            task.doLast(task1 -&gt;
                    System.out.println(<span class="hljs-string">"[run] Project name: "</span> + project.getName()));
        });
    }
}
</code></pre>
<p>Even with cache warnings enabled, task execution will fail<br /><code>fail2Task --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: fail2Task

&gt; Configure project :
[configuration] Project name: junit16727929338108926619

&gt; Task :fail2Task FAILED

2 problems were found storing the configuration cache.
- Task `:fail2Task` of type `org.gradle.api.DefaultTask`: cannot deserialize object of type 'org.gradle.api.Project' as these are not supported with the configuration cache.
  See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
- Task `:fail2Task` of type `org.gradle.api.DefaultTask`: cannot serialize object of type 'org.gradle.api.internal.project.DefaultProject', a subtype of 'org.gradle.api.Project', as these are not supported with the configuration cache.
  See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types

See the complete report at file:///tmp/junit16727929338108926619/build/reports/configuration-cache/47ehjfb227oo5kzbg615m5h3q/dghkqx053bip47mmv9kiov6f3/configuration-cache-report.html

[Incubating] Problems report is available at: file:///tmp/junit16727929338108926619/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':fail2Task'.
&gt; Cannot invoke "org.gradle.api.Project.getName()" because "project" is null

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':fail2Task'.
...    
Caused by: java.lang.NullPointerException: Cannot invoke "org.gradle.api.Project.getName()" because "project" is null
...

BUILD FAILED in 3s
1 actionable task: 1 executed
Configuration cache entry stored with 2 problems.
</code></pre>
<p>It failed with NullPointerException becuase, even on first run, it serialize and deserialize state, required for runtime blocks (in the separate task case above, the project call was a part of runtime action, not configuration).</p>
<p>Same as with task case, the fix is to store required data in a variable or using provider in configuration phase:</p>
<pre><code class="lang-java">    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{

        <span class="hljs-keyword">final</span> String projectName = project.getName();
        project.getTasks().register(<span class="hljs-string">"fail2Fix"</span>, task -&gt;  {
            task.doLast(task1 -&gt;
                    System.out.println(<span class="hljs-string">"[run] Project name: "</span> + projectName));
        });

        <span class="hljs-keyword">final</span> Provider&lt;String&gt; nameProvider = project.provider(() -&gt; {
            System.out.println(<span class="hljs-string">"[configuration] Provider called"</span>);
            <span class="hljs-keyword">return</span> project.getName();
        });
        project.getTasks().register(<span class="hljs-string">"fail2Fix2"</span>, task -&gt;  {
            task.doLast(task1 -&gt;
                    System.out.println(<span class="hljs-string">"[run] Project name: "</span> + nameProvider.get()));
        });
    }
</code></pre>
<p>Running <code>fail2Fix fail2Fix2 --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: fail2Fix fail2Fix2
[configuration] Provider called

&gt; Task :fail2Fix2
[run] Project name: junit17295962152626235093

&gt; Task :fail2Fix
[run] Project name: junit17295962152626235093

BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed
Configuration cache entry stored.
</code></pre>
<p>(the order of tasks is not determinate as they run in parallel)</p>
<p>Runtime blocks in plugin is the main “problem” for configuration cache: it applies much efforts analyzing them to serialize (only) required configuration state.</p>
<h2 id="heading-too-broad-serialization">Too broad serialization</h2>
<p>This is <strong>the most annoying problem</strong> as gradle can’t hint you about its source. For example, here is an extension with not serializable object (<code>SourceSet</code>):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail3Extension</span> </span>{

    <span class="hljs-keyword">public</span> Set&lt;SourceSet&gt; sets;
    <span class="hljs-keyword">public</span> String message;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Fail3Extension</span><span class="hljs-params">(Project project)</span> </span>{
        <span class="hljs-keyword">this</span>.sets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets();
    }
}
</code></pre>
<p>Plugin only use string extension property at runtime (and does not call not serializable property!):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail3Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        Fail3Extension ext = project.getExtensions().create(<span class="hljs-string">"fail3"</span>, Fail3Extension.class, project);

        project.getTasks().register(<span class="hljs-string">"fail3Task"</span>, task -&gt;  {
            task.doLast(task1 -&gt;
                    System.out.println(<span class="hljs-string">"[run] Message: "</span> + ext.message));
        });
    }
}
</code></pre>
<p>But gradle would try to serialize the <strong>entire extension</strong> here (object, referenced at runtime)<br /><code>fail3Task --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: fail3Task

&gt; Task :fail3Task
[run] Message: Configured!

2 problems were found storing the configuration cache.
- Task `:fail3Task` of type `org.gradle.api.DefaultTask`: cannot deserialize object of type 'org.gradle.api.tasks.SourceSetContainer' as these are not supported with the configuration cache.
  See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
- Task `:fail3Task` of type `org.gradle.api.DefaultTask`: cannot serialize object of type 'org.gradle.api.internal.tasks.DefaultSourceSetContainer', a subtype of 'org.gradle.api.tasks.SourceSetContainer', as these are not supported with the configuration cache.
  See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types

See the complete report at file:///tmp/junit10544361871289601409/build/reports/configuration-cache/5tucq48qd6ozznv386suhaql7/9u8dhuyl2ja1knarlvrf25z8q/configuration-cache-report.html

[Incubating] Problems report is available at: file:///tmp/junit10544361871289601409/build/reports/problems/problems-report.html

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
Configuration cache entry stored with 2 problems.
</code></pre>
<p>You can see here that gradle only points you to a not serializable type and you must analyze all runtime blocks yourself to understand the source of problem.</p>
<p>To fix this particular problem, reduce caches scope by assigninig exact required data to a variable in the configuration phase:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
    Fail3Extension ext = project.getExtensions().create(<span class="hljs-string">"fail3"</span>, Fail3Extension.class, project);

    project.getTasks().register(<span class="hljs-string">"fail3Task"</span>, task -&gt;  {
        <span class="hljs-comment">// reduce serialization scope</span>
        String message = ext.message;
        task.doLast(task1 -&gt;
                System.out.println(<span class="hljs-string">"[run] Message: "</span> + message));
    });
}
</code></pre>
<p>Here variable is assigned in the task’s <em>lazy initialization block</em> (and so user configuration is applied). It is important to not assign variable too early as user configuration may not be applied, like here:</p>
<pre><code class="lang-java"><span class="hljs-comment">// it would be NULL! Too early</span>
String message = ext.message;
project.getTasks().register(<span class="hljs-string">"fail3Task"</span>, task -&gt;  {
    task.doLast(task1 -&gt;
            System.out.println(<span class="hljs-string">"[run] Message: "</span> + message));
});
</code></pre>
<p>With provider the exact declaration place is not important:</p>
<pre><code class="lang-java">Provider&lt;String&gt; message = project.provider(() -&gt; ext.message);
project.getTasks().register(<span class="hljs-string">"fail3Task"</span>, task -&gt;  {
    task.doLast(task1 -&gt;
            System.out.println(<span class="hljs-string">"[run] Message: "</span> + message));
});
</code></pre>
<p>Local variable, cachable or non-cachable (<code>ValueSource</code>) provider usage is the magic wand for configuration cache compatiblity.</p>
<h2 id="heading-the-difference-of-plugin-and-task-serialization">The difference of plugin and task serialization</h2>
<p>Tasks and plugins are serialized differently! To show it, we will store not allowed object (<code>SourceSet</code>) in task and plugin fields, but <em>will not use it at runtime</em>.</p>
<p>The task:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail4Task</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">DefaultTask</span> </span>{

    <span class="hljs-keyword">private</span> SourceSet sourceSet;
    <span class="hljs-keyword">private</span> String name;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Fail4Task</span><span class="hljs-params">()</span> </span>{
        sourceSet = getProject().getExtensions().getByType(JavaPluginExtension.class).getSourceSets().getByName("main");
        name = sourceSet.getName();
    }

    <span class="hljs-meta">@TaskAction</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[run] Task source set: "</span> + name);
    }
}
</code></pre>
<p>The plugin:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail4Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-keyword">private</span> SourceSet sourceSet;

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        sourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().getByName(<span class="hljs-string">"test"</span>);

        project.getTasks().register(<span class="hljs-string">"fail4Task"</span>, Fail4Task.class, task -&gt;  {
            String set = sourceSet.getName();
            task.doLast(task1 -&gt;
                    System.out.println(<span class="hljs-string">"[run] Project source set: "</span> + set));
        });
    }
}
</code></pre>
<p>Run task <code>fail4Task --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: fail4Task

&gt; Task :fail4Task
[run] Task source set: main
[run] Project source set: test

2 problems were found storing the configuration cache.
- Task `:fail4Task` of type `ru.vyarus.gradle.plugin.fails.fail4.Fail4Task`: cannot deserialize object of type 'org.gradle.api.tasks.SourceSet' as these are not supported with the configuration cache.
  See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types
- Task `:fail4Task` of type `ru.vyarus.gradle.plugin.fails.fail4.Fail4Task`: cannot serialize object of type 'org.gradle.api.internal.tasks.DefaultSourceSet', a subtype of 'org.gradle.api.tasks.SourceSet', as these are not supported with the configuration cache.
  See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:disallowed_types

See the complete report at file:///tmp/junit3909871766538813794/build/reports/configuration-cache/ej5hwigx1x3swf7o2d6u9466r/6lf4x0dgeq19zwf53mjr1wh3t/configuration-cache-report.html

[Incubating] Problems report is available at: file:///tmp/junit3909871766538813794/build/reports/problems/problems-report.html

BUILD SUCCESSFU
1 actionable task: 1 executed
Configuration cache entry stored with 2 problems.
</code></pre>
<p>Gradle complains only about the task! So the task is serialized completely, but the plugin is not!</p>
<p>It would be easy to proove, just fixing the task:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fail4FixTask</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">DefaultTask</span> </span>{

    <span class="hljs-keyword">private</span> String name;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Fail4FixTask</span><span class="hljs-params">()</span> </span>{
        SourceSet sourceSet = getProject().getExtensions()
                .getByType(JavaPluginExtension.class).getSourceSets().getByName("main");
        name = sourceSet.getName();
    }

    <span class="hljs-meta">@TaskAction</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[run] Task source set: "</span> + name);
    }
}
</code></pre>
<p>Run <code>fail4Fix --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: fail4Fix

&gt; Task :fail4Fix
[run] Task source set: main
[run] Project source set: test

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>No problems! So you <strong>can use plugin’s private fields to store any data</strong> you need, just don’t reference it in runtime blocks (or wrap access with providers, producing something serializable).</p>
<h2 id="heading-listening-tasks">Listening tasks</h2>
<p>This is not stricly related to the configuration cache, but it would be important to highlight this behavor before the next chapter, covering build services.</p>
<p>Suppose you need to do somethig after a task execution (always), but you don’t control the task (3rd party plugin’s task). You have 2 (conf.cache - legal) ways: use <code>doLast</code> block and <a target="_blank" href="https://docs.gradle.org/current/userguide/build_services.html#operation_listener">build service as listener</a>.</p>
<p>The catch is: <code>doLast</code> block <strong>will not be called if task is not executed</strong>. Task is not executed when it’s <code>UP-TO-DATE</code>or <code>FROM-CACHE</code> (when <a target="_blank" href="https://docs.gradle.org/current/userguide/build_cache.html">build cache</a> enabled). So the only way to always detect task execution is by using build service.</p>
<p>Lets check. Here is a simple build service, listening for tasks:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Service</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BuildService</span>&lt;<span class="hljs-title">BuildServiceParameters</span>.<span class="hljs-title">None</span>&gt;,
        <span class="hljs-title">OperationCompletionListener</span> </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onFinish</span><span class="hljs-params">(FinishEvent finishEvent)</span> </span>{
        System.out.println(<span class="hljs-string">"[run] Finish event: "</span> + finishEvent.getDescriptor().getName());
    }
}
</code></pre>
<p>Task with output file (cachable):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample8Task</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">DefaultTask</span> </span>{

    <span class="hljs-meta">@OutputFile</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> Property&lt;File&gt; <span class="hljs-title">getOut</span><span class="hljs-params">()</span></span>;

    <span class="hljs-meta">@TaskAction</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
        System.out.println(<span class="hljs-string">"[run] Task executed"</span>);
        File out = getOut().get();

        BufferedWriter writer = <span class="hljs-keyword">new</span> BufferedWriter(<span class="hljs-keyword">new</span> FileWriter(out));
        writer.append(<span class="hljs-string">"Sample file content"</span>);
        writer.close();
    }
}
</code></pre>
<p>And plugin, declaring the task and configuring <code>doLast</code> block:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample8Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Inject</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> BuildEventsListenerRegistry <span class="hljs-title">getEventsListenerRegistry</span><span class="hljs-params">()</span></span>;

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        <span class="hljs-comment">// service listen for tasks</span>
        <span class="hljs-keyword">final</span> Provider&lt;Service&gt; service = project.getGradle().getSharedServices().registerIfAbsent(
                <span class="hljs-string">"service"</span>, Service.class);
        getEventsListenerRegistry().onTaskCompletion(service);

        project.getTasks().register(<span class="hljs-string">"sample8Task"</span>, Sample8Task.class, task -&gt; {
            task.getOut().set(project.getLayout().getBuildDirectory()
                        .dir(<span class="hljs-string">"sample8/out.txt"</span>).get().getAsFile());
            task.doLast(t -&gt; System.out.println(<span class="hljs-string">"[run] doLast for sample8Task"</span>));
        });
    }
}
</code></pre>
<p>Run <code>sample8Task --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: sample8Task

&gt; Task :sample8Task
[run] Task executed
[run] doLast for sample8Task
[run] Finish event: :sample8Task

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Both <code>doLast</code> and build service executed.</p>
<p>Run again <code>sample8Task --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.
&gt; Task :sample8Task UP-TO-DATE
Finish event: :sample8Task

BUILD SUCCESSFUL in 80ms
1 actionable task: 1 up-to-date
Configuration cache entry reused.
</code></pre>
<p>The tasks is <code>UP-TO-DATE</code> and so <code>doLast</code> was not called, but the build service was notified.</p>
<p>The only problem with build service is that it <strong>does not provide you a task instance</strong>, only task path. So you may need to store some additional task data, collected at configuration phase.</p>
<h2 id="heading-caching-data-in-build-service">Caching data in build service</h2>
<p>Build service cache parameters state <strong>at the time of service creation</strong>. Any further paramters modifications would not be serializaed (will be lost after the current tasks execution).</p>
<p>Sample service:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Service</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BuildService</span>&lt;<span class="hljs-title">Service</span>.<span class="hljs-title">Params</span>&gt; </span>{

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Service</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"Service created with state: "</span> + getParameters().getValues().get());
    }

    <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Params</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BuildServiceParameters</span> </span>{
        <span class="hljs-function">ListProperty&lt;String&gt; <span class="hljs-title">getValues</span><span class="hljs-params">()</span></span>;
    }
}
</code></pre>
<p>Plugin:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample7Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        <span class="hljs-keyword">final</span> List&lt;String&gt; values = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();

        Provider&lt;Service&gt; service = project.getGradle().getSharedServices().registerIfAbsent(
                <span class="hljs-string">"service"</span>, Service.class, spec -&gt; {
                    <span class="hljs-comment">// initial "persisted storage" value</span>
                    spec.getParameters().getValues().value(values);
                });

        values.add(<span class="hljs-string">"val1"</span>);
        values.add(<span class="hljs-string">"val2"</span>);

        project.getTasks().register(<span class="hljs-string">"sample7Task"</span>, task -&gt;
                task.doFirst(task1 -&gt;
                        System.out.println(<span class="hljs-string">"Task see state: "</span> + service.get().getParameters().getValues().get()))
        );
    }
}
</code></pre>
<p>Service will <strong>initialize only at runtime</strong> and so the collected configuration state would be preserved.</p>
<p>First run <code>sample7Task --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: sample7Task

&gt; Task :sample7Task
Service created with state: [val1, val2]
Task see state: [val1, val2]

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Second run <code>sample7Task --configuration-cache --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :sample7Task
Service created with state: [val1, val2]
Task see state: [val1, val2]

BUILD SUCCESSFUL in 67ms
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>State, prepared in plugin during configuration phase was serialized in service parameter.</p>
<p>What is important:</p>
<ul>
<li><p>Required state is collected in plugin field</p>
</li>
<li><p>The field is used to initialize service parameter (on creation)</p>
</li>
<li><p>Service is not initializard during configuration phase</p>
</li>
</ul>
<p>In the majority of cases, this trick (almost hack) would not be required becuase you can simply <strong>cache the value returned from service</strong> instead of re-recovering real service state. For example:</p>
<pre><code class="lang-java">project.getTasks().register(<span class="hljs-string">"sample7Task"</span>, task -&gt;
        <span class="hljs-comment">// would be cached, because its referenced in run block</span>
        List&lt;String&gt; state = service.get().getParameters().getValues().get()
        task.doFirst(task1 -&gt;
                System.out.println(<span class="hljs-string">"Task see state: "</span> + state))
);
</code></pre>
<p>Here service state would be cached and run from cache <strong>will not use the service</strong> at all.</p>
<p>The case when such caching is not possible is described below (as a real life example).</p>
<h3 id="heading-the-real-case">The real case</h3>
<p>The case I faced updating my <a target="_blank" href="https://github.com/xvik/gradle-quality-plugin">quality plugin</a>: the plugin is listening for quality tasks (from 3rd party plugins: pmd, checkstyle, findbugs, etc.) and print detected problems into console, using xml reports (produced by quality trasks). As you have seen above, the only way to always print console report after a task is to use build service (all quality tasks are cachable). But build service will receive just a task path, so information about reportable quality tasks must be collected under configuration phase (and cached).</p>
<p><code>doLast</code> block is called <strong>just after the task</strong> whereas build service (as task listener) could be called a bit later and <code>doLast</code> it is better for performing output (to print it just below the task output and inside of output of some other task, executed concurrently).</p>
<p>Using both methods (doLast and service) would lead to duplicate output, and so some “execution marker” must be stored somewhere to avoid duplicates. That’s why it would not be possible to just cache service state in a variable (in this case each task would have its own instance of cache and will not see an “execution marker”). So full service state recovery is required.</p>
<p>Simple service, which must contain the required information about tasks:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Service</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BuildService</span>&lt;<span class="hljs-title">Service</span>.<span class="hljs-title">Params</span>&gt;,
                    <span class="hljs-title">OperationCompletionListener</span> </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onFinish</span><span class="hljs-params">(FinishEvent finishEvent)</span> </span>{
        <span class="hljs-keyword">if</span> (finishEvent <span class="hljs-keyword">instanceof</span> TaskFinishEvent) {
            <span class="hljs-comment">// not important for example, just showing for completeness</span>
            TaskFinishEvent taskEvent = (TaskFinishEvent) finishEvent
            String taskPath = taskEvent.descriptor.taskPath;
            TaskDesc desc = getParameters().getValues().get().stream()
                .filter(it -&gt; it.path.equals(taskPath))
                .findFirst().orElse(<span class="hljs-keyword">null</span>)
            <span class="hljs-comment">// do something, knowing additional task data</span>
             <span class="hljs-keyword">if</span> (desc != <span class="hljs-keyword">null</span>) {
                <span class="hljs-keyword">if</span> (!desc.isCalled()) {
                    System.out.println(<span class="hljs-string">"Task "</span> + taskPath + <span class="hljs-string">" listened by service"</span>);
                    desc.setCalled(<span class="hljs-keyword">true</span>);
                } <span class="hljs-keyword">else</span> {
                    System.out.println(<span class="hljs-string">"Task "</span> + taskPath + <span class="hljs-string">" listened, but ignored"</span>);
                }
            }
        }
    }

    <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Params</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BuildServiceParameters</span> </span>{
        <span class="hljs-comment">// will hold all required data, prepared by plugin</span>
        <span class="hljs-function">ListProperty&lt;TaskDesc&gt; <span class="hljs-title">getValues</span><span class="hljs-params">()</span></span>;
    }
}
</code></pre>
<p><code>TaskDesc</code> is a simpe value class (with properties only):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskDesc</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Serializable</span> </span>{
    <span class="hljs-keyword">private</span> String name;
    <span class="hljs-keyword">private</span> String path;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> called;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">TaskDesc</span><span class="hljs-params">()</span> </span>{
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">TaskDesc</span><span class="hljs-params">(String name, String path)</span> </span>{
        <span class="hljs-keyword">this</span>.name = name;
        <span class="hljs-keyword">this</span>.path = path;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getName</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> name;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setName</span><span class="hljs-params">(String name)</span> </span>{
        <span class="hljs-keyword">this</span>.name = name;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getPath</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> path;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setPath</span><span class="hljs-params">(String path)</span> </span>{
        <span class="hljs-keyword">this</span>.path = path;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isCalled</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> called;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setCalled</span><span class="hljs-params">(<span class="hljs-keyword">boolean</span> called)</span> </span>{
        <span class="hljs-keyword">this</span>.called = called;
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">toString</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> name;
    }
}
</code></pre>
<p>The plugin register task and prepare a collection for caching in service parameter:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample7Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Inject</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> BuildEventsListenerRegistry <span class="hljs-title">getEventsListenerRegistry</span><span class="hljs-params">()</span></span>;

    <span class="hljs-comment">// collecting tasks info during configuration phase</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> List&lt;TaskDesc&gt; tasksInfo = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        <span class="hljs-comment">// service 1 with "state" in a private field</span>
        Provider&lt;Service&gt; service = project.getGradle().getSharedServices()
                .registerIfAbsent(<span class="hljs-string">"service"</span>, Service.class, spec -&gt; 
                    // on first creation, service will cache list value, but it is important
                    // to not create it too early
                    spec.getParameters().getValues().set(tasksInfo));

        getEventsListenerRegistry().onTaskCompletion(service);

        // tasks to demostrate behavior
        project.getTasks().register("task1", TrackedTask.class);
        project.getTasks().register("task2", TrackedTask.class);

        project.getTasks().withType(TrackedTask.class).configureEach(task -&gt; {
                captureTaskInfo(task);
                task.doLast(task1 -&gt; {
                    <span class="hljs-keyword">final</span> TaskDesc desc = service.get().getParameters().getValues().get().stream()
                            .filter(taskDesc -&gt; taskDesc.getPath().equals(task.getPath()))
                            .findAny().orElse(<span class="hljs-keyword">null</span>);
                    <span class="hljs-keyword">if</span> (!desc.isCalled()) {
                        System.out.println("Task " + task1.getName() + " doLast");
                        desc.setCalled(<span class="hljs-keyword">true</span>);
                    }
                });
        });
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">captureTaskInfo</span><span class="hljs-params">(Task task)</span> </span>{
         System.out.println(<span class="hljs-string">"Store task descriptor: "</span> + task.getName());
         tasksInfo.add(<span class="hljs-keyword">new</span> TaskDesc(task.getName(), task.getPath()));
    }
}
</code></pre>
<p>Creating cache <code>task1, task2, --configuration-cache, --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: task1 task2

&gt; Configure project :
Service configured: []
Store task descriptor: task1
Store task descriptor: task2

&gt; Task :task1
Service created with state: [task1, task2]
Task task1 doLast

&gt; Task :task2
Task task2 doLast
Task :task1 listened, but ignored
Task :task2 listened, but ignored

BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed
Configuration cache entry stored.
</code></pre>
<p>Run from cache <code>task1, task2, --configuration-cache, --configuration-cache-problems=warn</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
Service created with state: [task1, task2]
Task task1 doLast

&gt; Task :task2
Task task2 doLast
Task :task1 listened, but ignored
Task :task2 listened, but ignored

BUILD SUCCESSFUL in 74ms
2 actionable tasks: 2 executed
Configuration cache entry reused.
</code></pre>
<p>In this example sample task was not cachable and so here doLast was called, but, as you can see, listener is also called and has access to task data (and so in case of cachable task would work correctly).</p>
<p>For sure, this is an <strong>edge case</strong>, which you, most likely, would never face. Its here just to show what is possible under configuration cache.</p>
<h2 id="heading-multi-module-builds">Multi-module builds</h2>
<p>Configuration cache may force you to use build services, but it is important to keep in mind that you plugin might be used in a multi-module build.</p>
<p>If the plugin from parameters caching sample would be used in the multi-module build like this:</p>
<pre><code class="lang-java">plugins {
    id <span class="hljs-string">'com.mycompany.myplugin'</span> version <span class="hljs-string">'1.0'</span> apply <span class="hljs-keyword">false</span>
}

subprojects {
    apply plugin: <span class="hljs-string">'com.mycompany.myplugin'</span>
}
</code></pre>
<p>Then each module would create <strong>it’s own plugin instance</strong> with it’s own inner state. So the trick like this would not work anymore:</p>
<pre><code class="lang-java">    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        <span class="hljs-comment">// in multi-module project each module would create this list</span>
        <span class="hljs-keyword">final</span> List&lt;String&gt; values = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();

        <span class="hljs-comment">// but the service is global (initiated just once)</span>
        Provider&lt;Service&gt; service = project.getGradle().getSharedServices().registerIfAbsent(
                <span class="hljs-string">"service"</span>, Service.class, spec -&gt; {
                    spec.getParameters().getValues().value(values);
                });

        ...
    }
</code></pre>
<p>The service is global, and so <strong>only one configuration will be applied</strong> (from one subproject) and, as the result, it would contain the state, collected in one subproject only!</p>
<p>The simple workaround is to use separate service instances per project, by modifying service name:</p>
<pre><code class="lang-java">Provider&lt;Service&gt; service = project.getGradle().getSharedServices().registerIfAbsent(
        <span class="hljs-string">"service"</span> + project.getName(), Service.class, spec -&gt; {
             <span class="hljs-comment">// initial "persisted storage" value</span>
            spec.getParameters().getValues().value(values);
        });
</code></pre>
<p>For example, plugin in subproject “sub” would create service “servicesub”.</p>
<p>Note: you may have problems with the global service only in advanced cases when you have to store state in such service (in parameters, using plugin’s variable).</p>
<h3 id="heading-different-classpaths">Different classpaths</h3>
<p>Another possible multi-module case is if your plugin would be applied independently in submodules:</p>
<pre><code class="lang-java"><span class="hljs-comment">// pseudo configuration (in real life it would be different config files)</span>

subrojects(<span class="hljs-string">"sub1"</span>) {
    plugins {
        id <span class="hljs-string">'com.mycompany.myplugin'</span> version <span class="hljs-string">'1.0'</span>
    }
}

subrojects(<span class="hljs-string">"sub2"</span>) {
    plugins {
        id <span class="hljs-string">'com.mycompany.myplugin'</span> version <span class="hljs-string">'1.0'</span>
    }
}
</code></pre>
<p>Then plugins would be loaded in <strong>different classpaths</strong> and so they would not be able to use a global service (there would be “class cannot be cast to class” error becuase service classes would be <strong>different</strong> in modules).</p>
<p>Just pay attention to this moment - <strong>in most cases</strong>, project-scoped service is a better way.</p>
<h2 id="heading-jococo">Jococo</h2>
<p>When running TestKit-based tests with enabled jococo plugin (for coverage), you'll always have <a target="_blank" href="https://docs.gradle.org/8.14.3/userguide/configuration_cache.html#config_cache:not_yet_implemented:testkit_build_with_java_agent">an issue</a>:</p>
<pre><code class="lang-plaintext">1 problem was found storing the configuration cache.
- Gradle runtime: support for using a Java agent with TestKit builds is not yet implemented with the configuration cache.
  See https://docs.gradle.org/8.14.3/userguide/configuration_cache.html#config_cache:not_yet_implemented:testkit_build_with_java_agent
</code></pre>
<p>But, it's not a critical problem: test must check that it was THE ONLY problem:</p>
<pre><code class="lang-java">BuildResult result = run(<span class="hljs-string">'someTask'</span>, <span class="hljs-string">'--configuration-cache'</span>, <span class="hljs-string">'--configuration-cache-problems=warn'</span>);
Assertions.assertThat(result.getOutput()).contains(
                <span class="hljs-string">"1 problem was found storing the configuration cache"</span>,
                <span class="hljs-string">"Gradle runtime: support for using a Java agent with TestKit"</span>,
                <span class="hljs-string">"Calculating task graph as no cached configuration is available for tasks:"</span>
);
</code></pre>
<p>See <a target="_blank" href="https://github.com/xvik/learn-gradle-configuration-cache/blob/master/src/test/java/ru/vyarus/gradle/plugin/sample1/Sample1PluginKitTest.java">repository tests</a> for complete configuration cache testing example.</p>
<h2 id="heading-summary">Summary</h2>
<p>The summary <a target="_blank" href="https://blog.vyarus.ru/gradle-configuration-cache-by-example-part-1-behavior#heading-putting-all-together">does not change from part 1.</a></p>
<p>Just paying additional attention to the “magic wand” for configuration cache compatibilty:</p>
<ol>
<li><p>Assigning state to a variable (at correct time) could easilly reduce serialization scope</p>
</li>
<li><p>Provider could “move” code execution from runtime into configuration phase (so let you call not allowed staff at runtime).</p>
</li>
<li><p>ValueSource workarounds caching</p>
</li>
</ol>
<p>In most cases, its simpler to let gradle cache state (with variable or provider), instead of reproducing it under the cache.</p>
<p>it is safier to create build service per plugin instance, instead of global due to potential problems in multi-module projects.</p>
<p>Not allowed gradle projects replacements are perfectly <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:use_project_during_execution">described in the gradle docs</a>.<br />The only case in my practice that wasn’t described there was <code>project.getAntBuilder()</code> usage in task. In groovy plugin, the solution was to use groovy’s <code>AntBuilder</code> (not gradle builder) directly (<code>new AntBuilder()…</code>).<br />The runtime objects restrictions mostly driven by configuration state preserved in the disallowed objects, but direct usage of anything (not implicitly aware of gradle state) is completely fine.</p>
]]></content:encoded></item><item><title><![CDATA[Gradle configuration cache by example. Part 1: behavior]]></title><description><![CDATA[Gradle configuration cache becomaing mandatory in gradle 9. This affects all plugin authors as configuration cache must be supported by plugin explicitly (actually, plugin just should not do disallowed things).
Article split into two parts (kind of t...]]></description><link>https://blog.vyarus.ru/gradle-configuration-cache-by-example-part-1-behavior</link><guid isPermaLink="true">https://blog.vyarus.ru/gradle-configuration-cache-by-example-part-1-behavior</guid><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Mon, 08 Sep 2025 09:10:06 GMT</pubDate><content:encoded><![CDATA[<p>Gradle <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache.html">configuration cache</a> becomaing mandatory in gradle 9. This affects all plugin authors as <em>configuration cache must be supported by plugin explicitly</em> (actually, plugin just should not do disallowed things).</p>
<p>Article split into two parts (kind of theory and practice):</p>
<p>Part 1 shows how configuration cache works (by examples). It is the best way to learn it because, almost certainly, <strong>it works not like you think</strong> (if you never deal with it yourself).</p>
<p><a target="_blank" href="https://blog.vyarus.ru/gradle-configuration-cache-by-example-part-2-problems">Part 2</a> shows common problems (with example cases) and potential solutions.</p>
<h2 id="heading-preprequisites">Preprequisites</h2>
<p>I prepared a <a target="_blank" href="https://github.com/xvik/learn-gradle-configuration-cache">github repository</a> with samples. These samples include all described cases, but not exactly follow article samples (article describes cases separately for simplicity, whereas samples check multiple cases at once). You could clone this repo <strong>and experiment</strong> with <a target="_blank" href="https://github.com/xvik/learn-gradle-configuration-cache?tab=readme-ov-file#samples">samples</a> yourself!</p>
<p>As with any other cache, the configuration cache could be tested by two runs: the first run prepares cache record and the second run validates execution under the cache.</p>
<p>Repository samples use gradle <a target="_blank" href="https://docs.gradle.org/current/userguide/test_kit.html">TestKit</a> to verify cache behaviour:</p>
<pre><code class="lang-java"><span class="hljs-comment">// projectDir - temprorary directory (created per test)</span>
<span class="hljs-comment">// build.gradle file is created manually (with required test project configuration)</span>

<span class="hljs-comment">// run to create cache record</span>
BuildResult result = GradleRunner.create()
        .withProjectDir(projectDir)
        .withArguments(List.of(<span class="hljs-string">"myTask"</span>, <span class="hljs-string">"--configuration-cache"</span>))
        .withPluginClasspath()
        .forwardOutput()
        .build();

<span class="hljs-comment">// validation</span>
result.getOutput().contains(<span class="hljs-string">"Calculating task graph as no cached configuration is available for tasks"</span>);

<span class="hljs-comment">// run again to check cached behaviour</span>
result = GradleRunner.create()
        .withProjectDir(projectDir)
        .withArguments(List.of(<span class="hljs-string">"myTask"</span>, <span class="hljs-string">"--configuration-cache"</span>))
        .withPluginClasspath()
        .forwardOutput()
        .build();

<span class="hljs-comment">// validation</span>
result.getOutput().contains(<span class="hljs-string">"Reusing configuration cache"</span>);
</code></pre>
<p>On first execution gradle inidicates cache recording:</p>
<blockquote>
<p>Calculating task graph as no cached configuration is available for tasks: sample1Task<br />…<br />Configuration cache entry stored.</p>
</blockquote>
<p>On second run, gradle would indicate cache usage:</p>
<blockquote>
<p>Reusing configuration cache.<br />…<br />Configuration cache entry reused.</p>
</blockquote>
<h2 id="heading-the-executed-code">The executed code</h2>
<blockquote>
<p>The <strong>Configuration Cache</strong> builds on this idea of <em>work avoidance</em> and <em>parallelization</em>. When enabled, the Configuration Cache allows Gradle to skip the configuration phase entirely if nothing that affects the build configuration (such as build scripts) has changed. Additionally, Gradle applies performance optimizations to task execution.</p>
<p><a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:intro">gradle guide</a></p>
</blockquote>
<p>This means that under configuration cache, plugin’s code from configuration phase <strong>would not be executed</strong>.</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample1Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Plugin applied"</span>);

        <span class="hljs-comment">// register custom task</span>
        project.getTasks().register(<span class="hljs-string">"sampleTask"</span>, task -&gt; {
            System.out.println(<span class="hljs-string">"[configuration] Task configured"</span>);

            <span class="hljs-comment">// the only line that works also under the configuration cache</span>
            task.doFirst(task1 -&gt; System.out.println(<span class="hljs-string">"[run] Before task"</span>));
        });

        <span class="hljs-comment">// afterEvaluate often used by plugins as the first point where user configuration applied</span>
        project.afterEvaluate(p -&gt; System.out.println(<span class="hljs-string">"[configuration] Project evaluated"</span>));
    }
}
</code></pre>
<p>First execution: <code>sampleTask —configuration-cache</code></p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: sampleTask

&gt; Configure project :
[configuration] Plugin applied
[configuration] Project evaluated
[configuration] Task configured

&gt; Task :sampleTask
[run] Before task

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Run from cache: <code>sampleTask —configuration-cache</code></p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :sampleTask
[run] Before task

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>As you can see, the code, related to configuration phase, <strong>is not called</strong> at all. Paying attention: neither plugin, nor any task constructors <strong>would not be called</strong> under the cache (there is no configuration phase)!</p>
<h2 id="heading-the-state">The state</h2>
<blockquote>
<p>The configuration cache serialize configuration from the first execution and use it for later executions.</p>
</blockquote>
<p>Now we will use a simple extension to access user configuration in task:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample1Extension</span> </span>{
    <span class="hljs-keyword">public</span> String message = <span class="hljs-string">"Default"</span>;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Sample1Extension</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Extension created"</span>)
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getMessage</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-comment">// no prefix because it could be called in both phases</span>
        System.out.println(<span class="hljs-string">"Extension get message: "</span> + message);
        <span class="hljs-keyword">return</span> message;
    }
}
</code></pre>
<p>The plugin will become:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample1Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Plugin executed"</span>);
        Sample1Extension ext = project.getExtensions()
                    .create(<span class="hljs-string">"sample1"</span>, Sample1Extension.class);

        project.getTasks().register(<span class="hljs-string">"sampleTask"</span>, task -&gt; {
            task.doFirst(task1 -&gt; System.out.println(<span class="hljs-string">"[run] User message: "</span> + ext.message));
        });
    }
}
</code></pre>
<p>Project <code>build.gradle</code> would contain:</p>
<pre><code class="lang-java">sample1 {
    message = <span class="hljs-string">"hello user!"</span>
}
</code></pre>
<p>First execution: <code>sampleTask —configuration-cache</code></p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: sampleTask

&gt; Configure project :
[configuration] Plugin executed
[configuration] Extension created

&gt; Task :sampleTask
Extension get message: hello user!
[run] User message: hello user!

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Run from cache: <code>sampleTask —configuration-cache</code></p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :sampleTask
Extension get message: hello user!
[run] User message: hello user!

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>Pay attention: on the second run <em>extension constructor was not called</em>, but extension getter was!</p>
<p>What happend: on first execution, gradle tracked that <code>ext.message</code> value is used at runtime and so <strong>the entire</strong> <code>ext</code> object was serialized. On second run, the state was deserialized and the task used it for execution.</p>
<p>Now, what would happen if user configuration would change? Let’s run task the third time, but with updated <code>build.gradle</code></p>
<pre><code class="lang-plaintext">sample1 {
    message = "changed message!"
}
</code></pre>
<p>Run (configuration cache record already exists): <code>sampleTask —configuration-cache</code></p>
<pre><code class="lang-plaintext">Calculating task graph as configuration cache cannot be reused because file 'build.gradle' has changed.

&gt; Configure project :
[configuration] Plugin executed
[configuration] Extension created

&gt; Task :sampleTask
Extension get message: changed message!
[run] User message: changed message!

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Cache was <strong>invalidated</strong>, full configuration performed and a new cache record stored.</p>
<h3 id="heading-plugin-state">Plugin state</h3>
<p>Let’s see what would happen with plugin’s own fields:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample1Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-keyword">private</span> String pluginField;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Sample1Plugin</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Plugin created"</span>);
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        project.afterEvaluate(p -&gt; {
            pluginField = <span class="hljs-string">"assigned value"</span>;
            System.out.println(<span class="hljs-string">"[configuration] Project evaluated"</span>);
        });

        project.getTasks().register(<span class="hljs-string">"sampleTask"</span>, task -&gt; {
            task.doFirst(task1 -&gt; System.out.println(<span class="hljs-string">"[run] Before task: "</span> + pluginField));
        });
    }
}
</code></pre>
<p>Run <code>sampleTask —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: sample1Task

&gt; Configure project :
[configuration] Plugin created
[configuration] Project evaluated

&gt; Task :sampleTask
[run] Before task: assigned value

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Run from cache <code>sampleTask —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :sample1Task
[run] Before task: assigned value

BUILD SUCCESSFUL in 61ms
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>As you can see, plugin fields are serialized.</p>
<h3 id="heading-task-state">Task state</h3>
<p>Now, let’s see how task fields survive serialization. Use a separate task class:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample1Task</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">DefaultTask</span> </span>{

    <span class="hljs-meta">@Input</span>
    <span class="hljs-function"><span class="hljs-keyword">abstract</span> Property&lt;String&gt; <span class="hljs-title">getMessage</span><span class="hljs-params">()</span></span>;
    <span class="hljs-meta">@Input</span>
    <span class="hljs-function"><span class="hljs-keyword">abstract</span> Property&lt;String&gt; <span class="hljs-title">getMessage2</span><span class="hljs-params">()</span></span>;

    <span class="hljs-keyword">public</span> String field;
    <span class="hljs-keyword">private</span> String privateField;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Sample1Task</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Task created"</span>);
        privateField = <span class="hljs-string">"set"</span>;
    }

    <span class="hljs-meta">@TaskAction</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[run] Task executed: message="</span> + getMessage().get()
                + <span class="hljs-string">", message2="</span> + getMessage2().get()
                + <span class="hljs-string">", public field="</span> + field
                + <span class="hljs-string">", private field="</span> + privateField);
    }
}
</code></pre>
<p>Here we have 2 gradle properties and private and public fields. Plugin would become:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample1Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Plugin applied"</span>);
        <span class="hljs-keyword">final</span> Sample1Extension ext = project.getExtensions()
                                .create(<span class="hljs-string">"sample1"</span>, Sample1Extension.class);

        <span class="hljs-comment">// register custom task</span>
        project.getTasks().register(<span class="hljs-string">"sample1Task"</span>, Sample1Task.class, task -&gt; {
            task.getMessage().convention(ext.message);
            task.getMessage2().convention(<span class="hljs-string">"Default"</span>);
            task.field = <span class="hljs-string">"assigned value"</span>;
        });

        <span class="hljs-comment">// custom (lazy) task configuration</span>
        project.getTasks().withType(Sample1Task.class).configureEach(task -&gt; {
            task.getMessage2().set(<span class="hljs-string">"Custom"</span>);
        });
    }
}
</code></pre>
<p>Plugin:</p>
<ul>
<li><p>Set <code>message</code> to extension value</p>
</li>
<li><p>Set <code>message2</code> to “Custom” value (in lazy configuration block)</p>
</li>
<li><p>Public task field assigned to <code>assigned value</code></p>
</li>
</ul>
<p>The extension class remains the same as above. Configuration file is:</p>
<pre><code class="lang-plaintext">sample1 {
    message = "hello user!"
}
</code></pre>
<p>Run it two times <code>sample1Task —configuration-cache</code> (only run from cache is interesting):</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :sample1Task
Extension get message: hello user!
[run] Task executed: message=hello user!, message2=Custom, public field=assigned value, private field=set

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>As you can see:</p>
<ul>
<li><p>Task properties values are correct</p>
</li>
<li><p>Public and private field values are also preserved</p>
</li>
</ul>
<p>So, it is OK to <em>rely on task properties</em> under the configuration cache.</p>
<h2 id="heading-broken-uniquness">Broken uniquness</h2>
<p>One caveat with state serialization is <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:shared_objects">broken uniquness</a>: same object, used at multiple places would become a different objects after deserialization.</p>
<p>We will use a custom object to share state between tasks:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SharedState</span> </span>{

    <span class="hljs-comment">// show how externally assigned values survive</span>
    <span class="hljs-keyword">public</span> String direct;
    <span class="hljs-keyword">public</span> List&lt;String&gt; list = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">SharedState</span><span class="hljs-params">()</span> </span>{
        System.out.println(<span class="hljs-string">"[configuration] Shared state created: "</span> + System.identityHashCode(<span class="hljs-keyword">this</span>));
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">toString</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> System.identityHashCode(<span class="hljs-keyword">this</span>) + <span class="hljs-string">"@"</span> + list.toString() + <span class="hljs-string">", direct="</span> + direct;
    }
}
</code></pre>
<p>And the plugin:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample2Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        <span class="hljs-comment">// some object, common for two tasks</span>
        <span class="hljs-keyword">final</span> SharedState state = <span class="hljs-keyword">new</span> SharedState();
        <span class="hljs-comment">// some value directly assigned</span>
        state.direct = <span class="hljs-string">"Custom"</span>;

        project.getTasks().register(<span class="hljs-string">"task1"</span>).configure(task -&gt;
                task.doLast(task1 -&gt; {
                    state.list.add(<span class="hljs-string">"Task 1"</span>);
                    System.out.println(<span class="hljs-string">"[run] Task 1 shared object: "</span> + state);
                }));

        project.getTasks().register(<span class="hljs-string">"task2"</span>).configure(task -&gt;
                task.doLast(task1 -&gt; {
                    state.list.add(<span class="hljs-string">"Task 2"</span>);
                    System.out.println(<span class="hljs-string">"[run] Task 2 shared object: "</span> + state);
                }));
    }
}
</code></pre>
<p>Now run it without cache first <code>task1 task2</code>:</p>
<pre><code class="lang-plaintext">&gt; Configure project :
[configuration] Shared state created: 1353516483

&gt; Task :task1
[run] Task 1 shared object: 1353516483@[Task 1], direct=Custom

&gt; Task :task2
[run] Task 2 shared object: 1353516483@[Task 1, Task 2], direct=Custom

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
</code></pre>
<p>The same <code>SharedState</code> object instance is used in tasks.</p>
<p>Run with cache enabled: <code>task1 task2 —configuration-cache</code></p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: task1 task2

&gt; Configure project :
[configuration] Shared state created: 1202973804

&gt; Task :task2
[run] Task 2 shared object: 1650372210@[Task 2], direct=Custom

&gt; Task :task1
[run] Task 1 shared object: 724264550@[Task 1], direct=Custom

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry stored.
</code></pre>
<p>We could <strong>already</strong> see different instances in tasks (its just cache recording phase, but we already see side effects!): obviously, object was serialized and deserialized (because object constructor was called just once)</p>
<p>Running from cache <code>task1 task2 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
[run] Task 1 shared object: 1796870189@[Task 1], direct=Custom

&gt; Task :task2
[run] Task 2 shared object: 1139607728@[Task 2], direct=Custom

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry reused.
</code></pre>
<p>Again, two <strong>different instances</strong> used.</p>
<p>Paying attention: configuration phase execution with the <em>configuration cache enabled</em> is already different from usual gradle execution. But it’s for good: performing configuration state serialization and deserialization even on the first run, gradle reveals many cache-related problems (which otherwise would happen in the second execution (from cache)).</p>
<h2 id="heading-build-services">Build services</h2>
<p>The only way to workaround uniquness problem is <a target="_blank" href="https://docs.gradle.org/current/userguide/build_services.html#build_services">build services</a>.</p>
<blockquote>
<p>Ideally, there should be no need to share any data between tasks (in the majority of cases configuration cache should cache required data). But, sometimes, tasks communication is required at runtime.</p>
</blockquote>
<p>We will use a service to keep shared state between tasks:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SharedService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BuildService</span>&lt;<span class="hljs-title">SharedService</span>.<span class="hljs-title">Params</span>&gt;, <span class="hljs-title">AutoCloseable</span> </span>{

    <span class="hljs-keyword">public</span> String extParam;
    <span class="hljs-comment">// tasks might be executed in parallel (this simply avoids ConcurrentModificationException)</span>
    <span class="hljs-keyword">public</span> List&lt;String&gt; list = <span class="hljs-keyword">new</span> CopyOnWriteArrayList&lt;&gt;();

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">SharedService</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-comment">// could appear both in configuration and execution time</span>
        System.out.println(<span class="hljs-string">"Shared service created "</span> + System.identityHashCode(<span class="hljs-keyword">this</span>) + <span class="hljs-string">"@"</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Params</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BuildServiceParameters</span> </span>{
        <span class="hljs-function">Property&lt;String&gt; <span class="hljs-title">getExtParam</span><span class="hljs-params">()</span></span>;
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">toString</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> System.identityHashCode(<span class="hljs-keyword">this</span>) + <span class="hljs-string">"@"</span> + list.toString()
                + <span class="hljs-string">", param: "</span> + getParameters().getExtParam().getOrNull()
                + <span class="hljs-string">", field: "</span> + extParam;
    }

    <span class="hljs-comment">// IMPORTANT: gradle could close service at any time and start a new instance!</span>
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">close</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
        System.out.println(<span class="hljs-string">"Shared service closed: "</span> + System.identityHashCode(<span class="hljs-keyword">this</span>));
    }
}
</code></pre>
<p>The plugin would declare service and two simple tasks:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample3Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        <span class="hljs-comment">// IMPORTANT: service not created at this moment!</span>
        <span class="hljs-keyword">final</span> Provider&lt;SharedService&gt; service = project.getGradle().getSharedServices()
                .registerIfAbsent(<span class="hljs-string">"service"</span>, SharedService.class, spec -&gt; {
                        // configuration value set with parameter
                        spec.getParameters().getExtParam().convention("Default");
                    });

        // <span class="hljs-function">configuration value set <span class="hljs-title">DIRECTLY</span> <span class="hljs-params">(after service creation)</span>
        project.<span class="hljs-title">afterEvaluate</span><span class="hljs-params">(p -&gt; {
            service.get()</span>.extParam </span>= <span class="hljs-string">"Custom"</span>;
            System.out.println(<span class="hljs-string">"[configuration] Project evaluated"</span>);
        });

        project.getTasks().register(<span class="hljs-string">"task1"</span>).configure(task -&gt;
                task.doLast(task1 -&gt; {
                    <span class="hljs-keyword">final</span> SharedService sharedService = service.get();
                    sharedService.list.add(<span class="hljs-string">"Task 1"</span>);
                    System.out.println(<span class="hljs-string">"[run] Task 1 shared object: "</span> + sharedService);
                }));

        project.getTasks().register(<span class="hljs-string">"task2"</span>).configure(task -&gt; {
            <span class="hljs-comment">// just to disable tasks concurrency (simplier to verify output)</span>
            task.mustRunAfter(<span class="hljs-string">"task1"</span>);

            task.doLast(task1 -&gt; {
                <span class="hljs-keyword">final</span> SharedService sharedService = service.get();
                sharedService.list.add(<span class="hljs-string">"Task 2"</span>);
                System.out.println(<span class="hljs-string">"[run] Task 2 shared object: "</span> + sharedService);
            });
        });
    }
}
</code></pre>
<p>First run without cache <code>task1 task2</code>:</p>
<pre><code class="lang-plaintext">&gt; Configure project :
Shared service created 1202901166@
[configuration] Project evaluated

&gt; Task :task1
[run] Task 1 shared object: 1202901166@[Task 1], param: Default, field: Custom

&gt; Task :task2
[run] Task 2 shared object: 1202901166@[Task 1, Task 2], param: Default, field: Custom
Shared service closed: 1202901166

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
</code></pre>
<p>Shared service is created in <code>afterEvaluate</code> block (the first time <code>service.get()</code> was called) where its <code>extParam</code> field set to custom value.</p>
<p>The same service instance is used in both tasks. Service is closed after tasks execution.</p>
<p>Run with enabled cache <code>task1 task2 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: task1 task2

&gt; Configure project :
Shared service created 644777498@
[configuration] Project evaluated
Shared service closed: 644777498

&gt; Task :task1
Shared service created 1665614489@
[run] Task 1 shared object: 1665614489@[Task 1], param: Default, field: null

&gt; Task :task2
[run] Task 2 shared object: 1665614489@[Task 1, Task 2], param: Default, field: null
Shared service closed: 1665614489

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry stored.
</code></pre>
<p>Behavior is different:</p>
<ul>
<li><p>Service created in <code>afterEvaluate</code> block where its field is initialied</p>
</li>
<li><p>After configuration phase service <em>is closed!</em> This is intentional gradle behavior under the cache (gradle advice using services only at runtime)!</p>
</li>
<li><p>A new service instance is created before the first task execution. Direct field value is lost (<strong>service fields are not serialized!</strong>). Only parameter value preserved (it would be a value applied in service initialization block)</p>
</li>
</ul>
<p>Overall, uniquness preserved: the service instance is the same between tasks, but it is a different instance from configuration phase.</p>
<p>Run from cache <code>task1 task2 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
Shared service created 1651282902@
[run] Task 1 shared object: 1651282902@[Task 1], param: Default, field: null

&gt; Task :task2
[run] Task 2 shared object: 1651282902@[Task 1, Task 2], param: Default, field: null
Shared service closed: 1651282902

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry reused.
</code></pre>
<p>The same service instance used, but directly initialized service field value was lost (same as in previous run).</p>
<p>So under configuration cache, the same service instance would be used in run phase. Service, used on configuration phase, would be closed befor run phase.</p>
<h3 id="heading-prevent-service-closing">Prevent service closing</h3>
<p>There is a way to prevent service closing before a run phase: service should implement <code>OperationCompletionListener</code> (pretend it would <a target="_blank" href="https://docs.gradle.org/current/userguide/build_services.html#operation_listener">listen for tasks execution</a>):</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SharedService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BuildService</span>&lt;<span class="hljs-title">SharedServiceSingleton</span>.<span class="hljs-title">Params</span>&gt;, <span class="hljs-title">AutoCloseable</span>,
        <span class="hljs-title">OperationCompletionListener</span> </span>{

    ...

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onFinish</span><span class="hljs-params">(FinishEvent finishEvent)</span> </span>{
        System.out.println(<span class="hljs-string">"Finish event: "</span> + finishEvent.getDescriptor().getName() + <span class="hljs-string">" caught on service "</span> + <span class="hljs-keyword">this</span>);
    }
}
</code></pre>
<p>(the rest is [the same](#build-services))</p>
<p>Plugin should now register service as a listener:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample3Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Inject</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> BuildEventsListenerRegistry <span class="hljs-title">getEventsListenerRegistry</span><span class="hljs-params">()</span></span>;

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        ...
        <span class="hljs-keyword">final</span> Provider&lt;SharedServiceSingleton&gt; service = project.getGradle().getSharedServices().registerIfAbsent(
                <span class="hljs-string">"service"</span>, SharedServiceSingleton.class, spec -&gt; {
                    spec.getParameters().getExtParam().convention("Default");
                });
        // service listens <span class="hljs-keyword">for</span> tasks completion, which prevents gradle from stopping it in 
        // <span class="hljs-function">after configuration phase
        <span class="hljs-title">getEventsListenerRegistry</span><span class="hljs-params">()</span>.<span class="hljs-title">onTaskCompletion</span><span class="hljs-params">(service)</span></span>;

        ...
    }
}
</code></pre>
<p>Creating cache record <code>task1 task2 --configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: task1 task2

&gt; Configure project :
Shared service created 331815390@
[configuration] Project evaluated

&gt; Task :task1
[run] Task 1 shared object: 331815390@[Task 1], param: Default, field: Custom
Finish event: :task1 caught on service 331815390@[Task 1], param: Default, field: Custom

&gt; Task :task2
[run] Task 2 shared object: 331815390@[Task 1, Task 2], param: Default, field: Custom
Finish event: :task2 caught on service 331815390@[Task 1, Task 2], param: Default, field: Custom
Shared service closed: 331815390

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
Configuration cache entry stored.
</code></pre>
<p>This time the service was not closed and the same instance is used in configuration and run phases. As a result, service field value is preserved.</p>
<p>But, when it run from cache <code>task1 task2 --configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
Shared service created 1976530883@
[run] Task 1 shared object: 1976530883@[Task 1], param: Default, field: null
Finish event: :task1 caught on service 1976530883@[Task 1], param: Default, field: null

&gt; Task :task2
[run] Task 2 shared object: 1976530883@[Task 1, Task 2], param: Default, field: null
Finish event: :task2 caught on service 1976530883@[Task 1, Task 2], param: Default, field: null
Shared service closed: 1976530883

BUILD SUCCESSFUL in 53ms
2 actionable tasks: 2 executed
Configuration cache entry reused.
</code></pre>
<p>Service field will again be null because its value was <em>assigned in plugin under configuration phase</em>, which was missed here.</p>
<p>Usually, there is not much sense to use this trick (forcing singleton) because service state is not serialized, only parameters are. But, it make sense when the same instance as in configuration phase is required at runtime.</p>
<p>Moreover, gradle could close the service during runtime too: it tries to guess when service is not required anymore and close it. In complex cases, gradle could guess incorrectly and close service too early. Using the “listener trick” workarounds such cases.</p>
<h2 id="heading-method-calls">Method calls</h2>
<p>Here we will see if method calls are possible from runtime blocks:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample4Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        project.getTasks().register(<span class="hljs-string">"task1"</span>).configure(task -&gt; {            
            task.doLast(task1 -&gt; {
                System.out.println(<span class="hljs-string">"[run] Task exec: "</span> + computeMessage(<span class="hljs-string">"static"</span>));
            });
        });
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> String <span class="hljs-title">computeMessage</span><span class="hljs-params">(String source)</span> </span>{
        System.out.println(<span class="hljs-string">"called computeMessage('"</span> + source + <span class="hljs-string">"')"</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-string">"computed message "</span> + source;
    }
}
</code></pre>
<p>Creating cache entry <code>task1 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: task1

&gt; Task :task1
called computeMessage('static')
[run] Task exec: computed message static

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Run from cache <code>task1 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
called computeMessage('static')
[run] Task exec: computed message static

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>As you can see, the method is called under the cache. Note that method was <strong>not static</strong> - it is a plugin’s instance method.</p>
<p>I will not show it, but there could problems with calling method in <strong>groovy plugins</strong> due to its dynamic nature. So in groovy plugins, its better to call public static methods.</p>
<h2 id="heading-providers">Providers</h2>
<p>Provider is “a magic wand” for dealing with configuration cache problems: it workarounds runtime objects restriction (we will see it in part 2). Here we will only see how provider being cached.</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample4Plugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        project.getTasks().register(<span class="hljs-string">"task1"</span>).configure(task -&gt; {
            Provider&lt;String&gt; provider = project.provider(() -&gt; {
                String res = String.valueOf(project.findProperty(<span class="hljs-string">"param"</span>));
                System.out.println(<span class="hljs-string">"Provider called: "</span> + res);
                <span class="hljs-keyword">return</span> res;
            });
            task.doLast(task1 -&gt; {
                System.out.println(<span class="hljs-string">"[run] Task exec: "</span> + provider.get());
            });
        });
    }
}
</code></pre>
<p>Provider use configuration file property just to show how cache invalidates in this case.</p>
<p>First, run without cache <code>task1 -Pparam=1</code>:</p>
<pre><code class="lang-plaintext">&gt; Task :task1
Provider called: 1
Task exec: 1

BUILD SUCCESSFUL
1 actionable task: 1 executed
</code></pre>
<p>As expected, provider was called at runtime.</p>
<p>Run with cache enabled <code>task1 -Pparam=1 —configuration-cache</code></p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: task1
Provider called: 1

&gt; Task :task1
[run] Task exec: 1

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Pay attention that provider was <strong>called at configuration time</strong>!</p>
<p>Running from cache: <code>task1 -Pparam=1 —configuration-cache</code></p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
[run] Task exec: 1
</code></pre>
<p><strong>Provider not called</strong>! It’s value is already stored in cache. As a consequence, you can use any object inside provider because it is executed under configuration time!</p>
<p>And a bonus, what will happen if property value would change <code>task1 -Pparam=2 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as configuration cache cannot be reused because the set of Gradle properties has changed: the value of 'param' was changed.
Provider called: 2

&gt; Task :task1
[run] Task exec: 2

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>Gradle invalidated the cache.</p>
<p>Note that for system properties and environment variables <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:reading_sys_props_and_env_vars">special providers</a> should be used (gradle would not be able to detect change in this case).</p>
<h2 id="heading-always-called-providers">Always called providers</h2>
<p>Gradle provides a special kind of providers: <code>ValueSource</code> (it’s <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:external_processes">mentioned in docs</a>). In contrast to usual providers, value source implementations are always called (even under the cache).</p>
<p>Example source implementation:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NonCacheableValue</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ValueSource</span>&lt;<span class="hljs-title">String</span>, <span class="hljs-title">ValueSourceParameters</span>.<span class="hljs-title">None</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-keyword">public</span> <span class="hljs-meta">@Nullable</span> <span class="hljs-function">String <span class="hljs-title">obtain</span><span class="hljs-params">()</span> </span>{
        String val = System.getProperty(<span class="hljs-string">"foo"</span>);
        System.out.println(<span class="hljs-string">"NonCacheableValue: "</span> + val);
        <span class="hljs-keyword">return</span> val;
    }
}
</code></pre>
<p>The plugin:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sample4ValuePlugin</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Plugin</span>&lt;<span class="hljs-title">Project</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">apply</span><span class="hljs-params">(Project project)</span> </span>{
        project.getTasks().register(<span class="hljs-string">"task1"</span>).configure(task -&gt; {
            <span class="hljs-keyword">final</span> Provider&lt;String&gt; provider = project.getProviders()
                    .of(NonCacheableValue.class, spec -&gt; {});
            task.doLast(task1 -&gt; {
                System.out.println(<span class="hljs-string">"[run] Task exec: "</span> + provider.get());
            });
        });
    }
}
</code></pre>
<p>Run with enabled cache <code>task1 -Dfoo=1 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Calculating task graph as no cached configuration is available for tasks: task1

&gt; Task :task1
NonCacheableValue: 1
[run] Task exec: 1

BUILD SUCCESSFUL
1 actionable task: 1 executed
Configuration cache entry stored.
</code></pre>
<p>ValueSource (provider) <strong>called at runtime</strong> (in contrast to usual providers, which are called under configuration phase when cache is enabled).</p>
<p>Run from cache <code>task1 -Dfoo=1 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
NonCacheableValue: 1
[run] Task exec: 1

BUILD SUCCESSFUL in 73ms
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>ValueSource (provider) is called.</p>
<p>Now changing property value <code>task1 -Dfoo=2 —configuration-cache</code>:</p>
<pre><code class="lang-plaintext">Reusing configuration cache.

&gt; Task :task1
NonCacheableValue: 2
[run] Task exec: 2

BUILD SUCCESSFUL in 46ms
1 actionable task: 1 executed
Configuration cache entry reused.
</code></pre>
<p>No invalidation required - provider just provide the correct value.</p>
<h2 id="heading-putting-all-together">Putting all together</h2>
<p>When configuration cache is enabled, gradle would analyze runtime blocks and serialize related state.</p>
<ul>
<li><p>There are no restrictions at configuration time - all restrictions applied at runtime blocks</p>
</li>
<li><p>Plugin itself, task objects and any other objects would not be created under cache (required objects are deserialized)</p>
</li>
<li><p>Task and plugin fields are serialized and so availble on cached executions (see part 2 for more details)</p>
</li>
<li><p>Values from user configuration would be serialized (and so it does not matter for configuration cache if value was directly assigned or taken from the extension)</p>
</li>
<li><p>Object uniquness (when the same instance used in multiple places) could be lost due to deserializarion</p>
</li>
<li><p>Build services:</p>
<ul>
<li><p>Is the only way to keep unique (shared) data between tasks</p>
</li>
<li><p>Not serialized, so field values would be lost between executions</p>
</li>
<li><p>Remember only parameter values in time of the first service creation</p>
</li>
<li><p>Could be closed at any time</p>
</li>
<li><p>If service listen for tasks it would not be closed</p>
</li>
</ul>
</li>
<li><p>It is safe to call methods from runtime blocks (in groovy plugins better call public static methods)</p>
</li>
<li><p>Provider is called at configuration time (when cache is enabled) and so could use any gradle objects</p>
</li>
<li><p>ValueSource objects are never cached</p>
</li>
<li><p>Gradle tracks state change (like user configuration changes) and invalidate cache (but some <a target="_blank" href="https://docs.gradle.org/current/userguide/configuration_cache_requirements.html#config_cache:requirements:reading_sys_props_and_env_vars">edge cases</a> are possible)</p>
</li>
</ul>
<p><a target="_blank" href="https://blog.vyarus.ru/gradle-configuration-cache-by-example-part-2-problems">Read next the part 2 - problems</a></p>
]]></content:encoded></item><item><title><![CDATA[Debug gradle plugins]]></title><description><![CDATA[To debug a gradle plugin in the real project:

Open plugin project and add “remote” configuration (with defaults - it would be port 5005)

Run gradle: ./gradlew task -Dorg.gradle.debug=true (it will halt waiting for remote debugger attachment)

Run r...]]></description><link>https://blog.vyarus.ru/debug-gradle-plugins</link><guid isPermaLink="true">https://blog.vyarus.ru/debug-gradle-plugins</guid><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Tue, 02 Sep 2025 07:48:12 GMT</pubDate><content:encoded><![CDATA[<p>To debug a gradle plugin in the real project:</p>
<ul>
<li><p>Open plugin project and add “remote” configuration (with defaults - it would be port 5005)</p>
</li>
<li><p>Run gradle: <code>./gradlew task -Dorg.gradle.debug=true</code> (it will halt waiting for remote debugger attachment)</p>
</li>
<li><p>Run remote debug in plugin project</p>
</li>
</ul>
<p>All details are in the <a target="_blank" href="https://docs.gradle.org/current/userguide/troubleshooting.html#sec:troubleshooting_build_logic">gradle docs</a></p>
<p><strong>Note</strong>: in case of “port already in use” error, try to stop deamons first (<code>./gradlew —stop</code>) and, if it will not help, search for other java processes occuping the port (<code>lsof -i :5005</code>).</p>
]]></content:encoded></item><item><title><![CDATA[Publish snapshot into maven central from github action]]></title><description><![CDATA[Note: post covers only Gradle, but it could be easilly adopted for Maven (github action would be nearly the same - all important moments described; some maven-related links included)
Suppose you developed a library and related examples in a single re...]]></description><link>https://blog.vyarus.ru/publish-snapshot-into-maven-central-from-github-action</link><guid isPermaLink="true">https://blog.vyarus.ru/publish-snapshot-into-maven-central-from-github-action</guid><category><![CDATA[maven-central]]></category><category><![CDATA[gradle]]></category><category><![CDATA[github-actions]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Thu, 17 Jul 2025 10:43:48 GMT</pubDate><content:encoded><![CDATA[<p><strong>Note:</strong> post covers only <strong>Gradle</strong>, but it could be easilly adopted for Maven (github action would be nearly the same - all important moments described; some maven-related links included)</p>
<p>Suppose you developed a library and related examples in a single repository (e.g. examples stored as a separate project in the <code>examples</code> directory). It would be great to not only run library tests on CI, but also check examples compatibility with the latest snapshot. In theory it’s simple: run library tests, publish snapshot and run examples with a just published snapshot.</p>
<p>Previously, I have <a target="_blank" href="https://blog.vyarus.ru/using-github-packages-in-gradle-and-maven-projects">tried to use github packages for snapshots publication</a>, but this was far from perfect:</p>
<ol>
<li><p>Github packages are <strong>not</strong> accessible without authorization - users have to create their own github tokens in order to access snapshots.</p>
</li>
<li><p>Each published snapshot is counted as a separate package and so, after some time, you have to clean up outdated versions <strong>manually</strong> (becuase storage space is limited)</p>
</li>
</ol>
<p>Recently, sonatype <a target="_blank" href="https://central.sonatype.org/pages/ossrh-eol/">sunset OSSRH publication</a> so I have to update my maven central publications and reviewed my snapshots approach.</p>
<p>Maven central <a target="_blank" href="https://central.sonatype.org/publish/publish-portal-snapshots/">snapshots publication</a> specifics:</p>
<ol>
<li><p>Snapshot is <strong>removed</strong> after 90 days (more than enough)</p>
</li>
<li><p><strong>No signing</strong> is required (and overall publication validation is disabled)</p>
</li>
<li><p>Published snapshots are available <strong>immediately</strong> (in contrast to releases, which are available after ~1h)</p>
</li>
<li><p>Users would have to use a <strong>custom repository</strong> to access snapshots (but <strong>without</strong> required authorization!)</p>
</li>
</ol>
<h2 id="heading-sonatype-configuration">Sonatype configuration</h2>
<p><strong>Important:</strong> It is assumed that your namespace was already migrated (migration ended on 30th of June 2025). If you did not perform migration manually, it would be performed automatically (in some time).</p>
<p>First of all, snapshots must be <a target="_blank" href="https://central.sonatype.org/publish/publish-portal-snapshots/">enabled for your namespace</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752741781265/20262f20-6776-4761-8e10-b4dc8acc452c.png" alt class="image--center mx-auto" /></p>
<p>It is also important to <a target="_blank" href="https://central.sonatype.org/publish/generate-portal-token/">generate a new token</a>. Even if you already have token, generated before June 30th, <strong>it’s better to re-generate it</strong> (there are many messages on the sonatype forum about 401 problems with the old tokens).</p>
<p>Open <a target="_blank" href="https://central.sonatype.com/account">https://central.sonatype.com/account</a> and hit “generate token”. Copy generated tokens (user and password tokens) because window will close in 1 minute.</p>
<h2 id="heading-project-configuration">Project configuration</h2>
<p>In spite of the fact that OSSRH is shut down, sonatype provides a <a target="_blank" href="https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/">OSSRH-compatible publication API</a>. With it you can just change target maven repositories and don’t need to search for new plugins.</p>
<p>I use gradle <a target="_blank" href="https://github.com/gradle-nexus/publish-plugin">maven-publish</a> plugin. In my case, configuration looks like:</p>
<pre><code class="lang-java">nexusPublishing {
    repositories {
        sonatype {
            nexusUrl = uri(<span class="hljs-string">"https://ossrh-staging-api.central.sonatype.com/service/local/"</span>)
            snapshotRepositoryUrl = uri(<span class="hljs-string">"https://central.sonatype.com/repository/maven-snapshots/"</span>)
            username = findProperty(<span class="hljs-string">'sonatypeUser'</span>)
            password = findProperty(<span class="hljs-string">'sonatypePassword'</span>)
        }
    }
}
</code></pre>
<p>Only urls are important here. The plugin configures <code>sonatype</code> maven publication, which is only important for snapshot publication (you can configure maven publication manually - only release would require additinoal actions, handled by this plugin).</p>
<p>Locally, <code>sonatypeUser</code> and <code>sonatypePassword</code> properties could be specified in the global gradle properties file (<code>~/.gradle/gradle.properties</code>). On CI, environment variables would be used (<code>ORG_GRADLE_PROJECT_sonatypeUser</code> and <code>ORG_GRADLE_PROJECT_sonatypePassword</code>).</p>
<p>For the examples project, it is required to configure a <a target="_blank" href="https://central.sonatype.org/publish/publish-portal-snapshots/#consuming-via-gradle">custom repository</a> to access snapshots:</p>
<pre><code class="lang-java">repositories {
    mavenLocal()
    mavenCentral()
    maven {
        name = <span class="hljs-string">'Central Portal Snapshots'</span>
        url = <span class="hljs-string">'https://central.sonatype.com/repository/maven-snapshots/'</span>
        mavenContent {
            snapshotsOnly()
            includeGroupAndSubgroups(<span class="hljs-string">'ru.vyarus'</span>)
        }
    }
}
</code></pre>
<p>For security reasons, repository should be limited to snapshots and one group (in my case sub groups are aslo included because they are used by library modules).</p>
<h2 id="heading-github-actions">Github actions</h2>
<h3 id="heading-credentials">Credentials</h3>
<p>Generated sonatype tokens must be declared as a github repositry secrets:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752743582930/8e17a68a-295e-4fa4-9b82-0ae9062cfd9d.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-actions">Actions</h3>
<p>For simplicity, my script split into 3 files:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752744720619/78839fcd-7219-49f4-83f0-37918f770a97.png" alt class="image--center mx-auto" /></p>
<p>(ignore <code>dependencies.yml</code> - it updates project dependencies for github dependency graph)</p>
<p>CI script runs build matrix for multiple java versions: check build, run tests (publish coverage data). Also, CI script runs snapshot publication and examples workflows, if required:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">CI</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
  <span class="hljs-attr">pull_request:</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Java</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.java</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">fail-fast:</span> <span class="hljs-literal">false</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">java:</span> [<span class="hljs-number">11</span>, <span class="hljs-number">17</span>, <span class="hljs-number">21</span>]
    <span class="hljs-attr">outputs:</span>
      <span class="hljs-attr">version:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.project.outputs.version</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">JDK</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.java</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-java@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">distribution:</span> <span class="hljs-string">'zulu'</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.java</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">gradle/actions/setup-gradle@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          chmod +x gradlew
          ./gradlew assemble --no-daemon
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">./gradlew</span> <span class="hljs-string">check</span> <span class="hljs-string">--no-daemon</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Extract</span> <span class="hljs-string">Project</span> <span class="hljs-string">version</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">'project'</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          ver=$(./gradlew :properties --property version --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')
          echo "Project version: $ver"
          echo "version=$ver" &gt;&gt; $GITHUB_OUTPUT
</span>
  <span class="hljs-attr">publish:</span>
    <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/dw-3'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.event_name</span> <span class="hljs-type">!=</span> <span class="hljs-string">'pull_request'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">endsWith(needs.build.outputs.version,</span> <span class="hljs-string">'-SNAPSHOT'</span><span class="hljs-string">)</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">needs:</span> <span class="hljs-string">build</span>
    <span class="hljs-attr">uses:</span> <span class="hljs-string">./.github/workflows/publish-snapshot.yml</span>
    <span class="hljs-comment"># workflow can't see secrets directly</span>
    <span class="hljs-attr">secrets:</span>
      <span class="hljs-attr">sonatype_user:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.SONATYPE_USERNAME</span> <span class="hljs-string">}}</span>
      <span class="hljs-attr">sonatype_password:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.SONATYPE_PASSWORD</span> <span class="hljs-string">}}</span>

  <span class="hljs-attr">examples:</span>
    <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/dw-3'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.event_name</span> <span class="hljs-type">!=</span> <span class="hljs-string">'pull_request'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">endsWith(needs.build.outputs.version,</span> <span class="hljs-string">'-SNAPSHOT'</span><span class="hljs-string">)</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">needs:</span> [<span class="hljs-string">build</span>, <span class="hljs-string">publish</span>]
    <span class="hljs-attr">uses:</span> <span class="hljs-string">./.github/workflows/examples-CI.yml</span>
</code></pre>
<p>Snapshot publication must not be performed for pull requests, forks and on release (including release tag).</p>
<p>In order to prevent snapshot publication for release we need to <a target="_blank" href="https://stackoverflow.com/a/48616954/5186390">know project version</a>:</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Extract</span> <span class="hljs-string">Project</span> <span class="hljs-string">version</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">'project'</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          ver=$(./gradlew :properties --property version --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')
          echo "Project version: $ver"
          echo "version=$ver" &gt;&gt; $GITHUB_OUTPUT</span>
</code></pre>
<p><strong>Note:</strong> <code>—property version</code> works starting from Gradle 7.5. For older gradle versions, this part could be removed and still the correct version value would be selected.</p>
<p>(for maven, <a target="_blank" href="https://stackoverflow.com/a/67931765/5186390">help:evaluate could be used</a> for version extraction: <code>mvn help:evaluate -Dexpression=project.version -q -DforceStdout</code>)</p>
<p><code>echo "version=$ver" &gt;&gt; $GITHUB_OUTPUT</code> publish extracted version as a build step output. Note that version is published for each matrix step (it’s almost immediate, so not an issue; just in case, if you want to store separate data from matrix steps, see <a target="_blank" href="https://github.com/orgs/community/discussions/17245#discussioncomment-11222880">this post</a>).</p>
<p>In order to use stored version in the separate job, we should declare it as a <code>build</code> job’s output:</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">outputs:</span>
      <span class="hljs-attr">version:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.project.outputs.version</span> <span class="hljs-string">}}</span>
</code></pre>
<p>After successful build, snapshot publication run from the separate file:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">publish:</span>
    <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/dw-3'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.event_name</span> <span class="hljs-type">!=</span> <span class="hljs-string">'pull_request'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">endsWith(needs.build.outputs.version,</span> <span class="hljs-string">'-SNAPSHOT'</span><span class="hljs-string">)</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">needs:</span> <span class="hljs-string">build</span>
    <span class="hljs-attr">uses:</span> <span class="hljs-string">./.github/workflows/publish-snapshot.yml</span>
    <span class="hljs-comment"># workflow can't see secrets directly</span>
    <span class="hljs-attr">secrets:</span>
      <span class="hljs-attr">sonatype_user:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.SONATYPE_USERNAME</span> <span class="hljs-string">}}</span>
      <span class="hljs-attr">sonatype_password:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.SONATYPE_PASSWORD</span> <span class="hljs-string">}}</span>
</code></pre>
<p><code>needs: build</code> would hold this job until <code>build</code> job completion</p>
<p><code>if</code> would allow snapshot publication only for direct branch commit and only for snapshot versions. Note that it reference build job outputs, referenced from needs (<code>needs.build.outputs.version</code>)</p>
<p>It is important to bypass required secrets here because they would not be availble in a separate worklow (executed under <code>workflow_call</code>). It was also possible to just <a target="_blank" href="https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/sharing-automations/reuse-workflows#using-inputs-and-secrets-in-a-reusable-workflow">inherit secrets</a>.</p>
<h3 id="heading-snapshot-publication">Snapshot publication</h3>
<p><code>publish-snapshot.yml</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">snapshot</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">workflow_call:</span>
    <span class="hljs-attr">secrets:</span>
      <span class="hljs-attr">sonatype_user:</span>
        <span class="hljs-attr">required:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">sonatype_password:</span>
        <span class="hljs-attr">required:</span> <span class="hljs-literal">true</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">publish:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">snapshot</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">JDK</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-java@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">distribution:</span> <span class="hljs-string">'zulu'</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-number">17</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">gradle/actions/setup-gradle@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">without</span> <span class="hljs-string">tests</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          chmod +x gradlew
          ./gradlew build -x check --no-daemon
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">ORG_GRADLE_PROJECT_sonatypeUser:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.sonatype_user</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">ORG_GRADLE_PROJECT_sonatypePassword:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.sonatype_password</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">./gradlew</span> <span class="hljs-string">publishToSonatype</span>
</code></pre>
<p>This workflow is triggered by the main script and can’t access secrets (github actions restriction) and so required secrets <a target="_blank" href="https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/sharing-automations/reuse-workflows#using-inputs-and-secrets-in-a-reusable-workflow">must be declared</a>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">on:</span>
  <span class="hljs-attr">workflow_call:</span>
    <span class="hljs-attr">secrets:</span>
      <span class="hljs-attr">sonatype_user:</span>
        <span class="hljs-attr">required:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">sonatype_password:</span>
        <span class="hljs-attr">required:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>As it is a seprate workflow, we have to checkout and build project again (but without tests now):</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">without</span> <span class="hljs-string">tests</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          chmod +x gradlew
          ./gradlew build -x check --no-daemon</span>
</code></pre>
<p>And, finally, we need to call <code>publishToSonatype</code> task to publish snapshot:</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">ORG_GRADLE_PROJECT_sonatypeUser:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.sonatype_user</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">ORG_GRADLE_PROJECT_sonatypePassword:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.sonatype_password</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">./gradlew</span> <span class="hljs-string">publishToSonatype</span>
</code></pre>
<p>Note that required credentials passed as environment variables (<code>ORG_GRADLE_PROJECT_sonatypeUser</code>), which gradle would apply as a project properties value.</p>
<p>After successful snapshot publication, examples could be run</p>
<h3 id="heading-examples-run">Examples run</h3>
<p><code>examples-CI.yml</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Examples</span> <span class="hljs-string">CI</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">workflow_call:</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">defaults:</span>
      <span class="hljs-attr">run:</span>
        <span class="hljs-attr">working-directory:</span> <span class="hljs-string">examples</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Java</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.java</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">java:</span> [<span class="hljs-number">11</span>, <span class="hljs-number">17</span>]

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">JDK</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.java</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-java@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">distribution:</span> <span class="hljs-string">'zulu'</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.java</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">gradle/actions/setup-gradle@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">Check</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          chmod +x gradlew
          ./gradlew build --no-daemon</span>
</code></pre>
<p>Again, as we have a separate workflow, project must be cheked out again.</p>
<p>Default directory changed to <code>examples</code> where separate gradle project is stored:</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">defaults:</span>
      <span class="hljs-attr">run:</span>
        <span class="hljs-attr">working-directory:</span> <span class="hljs-string">examples</span>
</code></pre>
<p>Correct snapshot version and required maven repository is configured in the examples project.</p>
<h3 id="heading-dynamic-library-version">Dynamic library version</h3>
<p>Scripts, descibed above, assume that actual library snapshot version is specified in the examples project. But we already extracted an actual project version, so it is possible to use for the examples project run (to avoid manual version changes after each release).</p>
<p>Declare custom <a target="_blank" href="https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/sharing-automations/reuse-workflows#using-inputs-and-secrets-in-a-reusable-workflow">input</a> for examples workflow (<code>examples-CI.yml</code>):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">on:</span>
  <span class="hljs-attr">workflow_call:</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-attr">version:</span>
        <span class="hljs-attr">required:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
</code></pre>
<p>And assign provided input to the environment variable:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">Check</span>
  <span class="hljs-attr">env:</span>
    <span class="hljs-attr">ORG_GRADLE_PROJECT_guiceyBom:</span> <span class="hljs-string">${{</span> <span class="hljs-string">inputs.version</span> <span class="hljs-string">}}</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">|
    chmod +x gradlew
    ./gradlew build --no-daemon</span>
</code></pre>
<p>The main CI workflow (<code>CI.yml</code>) must specify library version for examples workflow:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">examples:</span>
  <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/dw-3'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.event_name</span> <span class="hljs-type">!=</span> <span class="hljs-string">'pull_request'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">endsWith(needs.build.outputs.version,</span> <span class="hljs-string">'-SNAPSHOT'</span><span class="hljs-string">)</span> <span class="hljs-string">}}</span>
  <span class="hljs-attr">needs:</span> [<span class="hljs-string">build</span>, <span class="hljs-string">publish</span>]
  <span class="hljs-attr">uses:</span> <span class="hljs-string">./.github/workflows/examples-CI.yml</span>
  <span class="hljs-attr">with:</span>
    <span class="hljs-attr">version:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.build.outputs.version</span> <span class="hljs-string">}}</span>
</code></pre>
<p>In my case, required library version was declared explicitly in <code>build.gradle</code>:</p>
<pre><code class="lang-java">ext {
    guiceyBom = <span class="hljs-string">'6.3.2'</span>
    dwVersion = <span class="hljs-string">'3.0.14'</span>
}
</code></pre>
<p>But this <strong>would override</strong> the environment variable (<code>ORG_GRADLE_PROJECT_guiceyBom</code>) value. To workaround it, assigning default only if value is not already set:</p>
<pre><code class="lang-java">ext {
    guiceyBom = findProperty(<span class="hljs-string">'guiceyBom'</span>) ?: <span class="hljs-string">'6.3.2'</span>
    dwVersion = <span class="hljs-string">'3.0.14'</span>
}
</code></pre>
<p>This way, it will use environment variable on CI and the default value for local runs.</p>
<h3 id="heading-summary">Summary</h3>
<p>Action execution would look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752746052794/a9fc27ee-f7ef-4b40-a1d1-43de1dd598cd.png" alt class="image--center mx-auto" /></p>
<p>Publication waits build copletion (in case of build error - no need to publish snapshot). And examples wait for snapshot publication (and so will not run if publication fails).</p>
<p>Full scripts (with dynamic library version for examples run) <a target="_blank" href="https://github.com/xvik/dropwizard-guicey/tree/dw-3/.github/workflows">could be found here</a></p>
<p>More details could be found in <a target="_blank" href="https://simonscholz.dev/tutorials/publish-maven-central-gradle">this blog post</a>, which helped me a lot.</p>
]]></content:encoded></item><item><title><![CDATA[Using github packages in Gradle and Maven projects]]></title><description><![CDATA[Github packages is an easy way to publish current snapshot versions. The biggest problem with it is access token requirement for accessing published packages (no free access).
Yes, I know, there is a super simple jitpack for this, but it can’t be use...]]></description><link>https://blog.vyarus.ru/using-github-packages-in-gradle-and-maven-projects</link><guid isPermaLink="true">https://blog.vyarus.ru/using-github-packages-in-gradle-and-maven-projects</guid><category><![CDATA[Github Packages]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[gradle]]></category><category><![CDATA[maven]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Sun, 19 Jan 2025 07:39:22 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages">Github packages</a> is an easy way to publish current snapshot versions. The biggest problem with it is <strong>access token requirement</strong> for accessing published packages (no free access).</p>
<p>Yes, I know, there is a super simple <a target="_blank" href="https://jitpack.io/">jitpack</a> for this, but it can’t be used in some cases because instead of using your build output it searches for packages with some euristic. Moreover, jitpack generates its own BOM, overwriting your own. So for multi-module (or projects with more complex build) projects jitpack is not an option.</p>
<p>The biggest advantage of <strong>github packages</strong> is its simple integration with <strong>github actions</strong> (super simple publication). This covers one important CI use case: test commit - publish new snapshot - use snapshot to run tests.</p>
<h2 id="heading-using-already-published-package">Using already published package</h2>
<h3 id="heading-create-access-token">Create access token</h3>
<p>On any github page, in the upper top corner, click on your profile icon and select <strong>Settings</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737267560262/12c30fe1-a065-4741-814e-b8bb7c2ffbbb.png" alt class="image--center mx-auto" /></p>
<p>On the left bar click <strong>Developer setting</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737267673066/9f275e12-246f-4a70-8d18-b1d8eb80f101.png" alt class="image--center mx-auto" /></p>
<p>Click <strong>Tokens (classic)</strong> under <strong>Personal access tokens</strong> section</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737267834568/36c327c9-a7e7-407e-b30d-eaee32ed5005.png" alt class="image--center mx-auto" /></p>
<p>Choose <strong>Generate new token (classic)</strong> in the Generate new token dropdown</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737267897391/49400b2f-b216-49ff-a3b5-e694a891e6f0.png" alt class="image--center mx-auto" /></p>
<p>Most likely, github would ask you to confirm authentication here.</p>
<p>Set token description. Set token to never expire because this token will only grant a read right on github packages (moreover, you’ll be able to use the same token to access any github packages repository)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737268073854/7364fff8-8cf5-4024-9e58-d40e472822c7.png" alt class="image--center mx-auto" /></p>
<p>Select ONLY <strong>read:packages</strong> permission.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737268232448/e2f1bde0-e224-4583-9a23-4f0a534d7e13.png" alt class="image--center mx-auto" /></p>
<p>Of course, token with any other rights added will also work, but if you do so consider setting token expiration (as such token stealing might be sensitive).</p>
<p><strong>IMPORTANT Github will show you token just once</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737268539399/0e044e2e-139e-4e20-9dea-387f1326f4f2.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-gradle-project-configuration">Gradle project configuration</h3>
<p>Add github packages repository either in <strong>build.gradle</strong> :</p>
<pre><code class="lang-java">repositories {
    mavenCentral()
    maven {
        url  = <span class="hljs-string">'https://maven.pkg.github.com/user/project'</span>
        credentials {
            username = findProperty(<span class="hljs-string">'gpr.user'</span>) ?: System.getenv(<span class="hljs-string">"USERNAME"</span>)
            password = findProperty(<span class="hljs-string">'gpr.key'</span>) ?: System.getenv(<span class="hljs-string">"TOKEN"</span>)
        }
    }
}
</code></pre>
<p>OR in <strong>settings.gradle</strong></p>
<pre><code class="lang-java">dependencyResolutionManagement {
    repositories {
        mavenCentral()
        maven {
            url  = <span class="hljs-string">'https://maven.pkg.github.com/user/project'</span>
            credentials {
                username = settings.ext.find(<span class="hljs-string">'gpr.user'</span>) ?: System.getenv(<span class="hljs-string">"USERNAME"</span>)
                password = settings.ext.find(<span class="hljs-string">'gpr.key'</span>) ?: System.getenv(<span class="hljs-string">"TOKEN"</span>)
            }
        }
    }
}
</code></pre>
<p>Note: <strong>System.getenv</strong> part will be used if you’re goinf to run this project on github actions to consume credentials directly from environment variables. If you don’t plan to run project on CI, you can remove this section (see complete description at the bottom).</p>
<p>Put you access token in the global gradle configuration: <strong>~/.gradle/gradle.properties</strong></p>
<pre><code class="lang-java">gpr.user=&lt;your github user name&gt;
gpr.key=&lt;your github classic token&gt;
</code></pre>
<h3 id="heading-maven-project-configuration">Maven project configuration</h3>
<p>Add credentials into <strong>~/.m2/settings.xml</strong></p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">settings</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://maven.apache.org/SETTINGS/1.0.0"</span>
          <span class="hljs-attr">xmlns:xsi</span>=<span class="hljs-string">"http://www.w3.org/2001/XMLSchema-instance"</span>
          <span class="hljs-attr">xsi:schemaLocation</span>=<span class="hljs-string">"http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd"</span>&gt;</span>            

    <span class="hljs-tag">&lt;<span class="hljs-name">servers</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">server</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>github<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">username</span>&gt;</span>USERNAME<span class="hljs-tag">&lt;/<span class="hljs-name">username</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">password</span>&gt;</span>TOKEN<span class="hljs-tag">&lt;/<span class="hljs-name">password</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">server</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">servers</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">settings</span>&gt;</span>
</code></pre>
<p>Add github packages repository (directly in project or in profile inside settings.xml)</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">repositories</span>&gt;</span>    
  <span class="hljs-tag">&lt;<span class="hljs-name">repository</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>github<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>https://maven.pkg.github.com/user/project<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">snapshots</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">enabled</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">enabled</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">snapshots</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">repository</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">repositories</span>&gt;</span>
</code></pre>
<p>IMPORTANT: <strong>repository id</strong> must be the same as <strong>server id</strong> in settings.xml (to apply configured authentication)</p>
<h2 id="heading-publishing-package-in-github-action">Publishing package in github action</h2>
<p>Will show publishing only for gradle project. No special repository configuration is required - just publish new package.</p>
<p>In <strong>build.gradle</strong> add github packages repository:</p>
<pre><code class="lang-java"> <span class="hljs-comment">// https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-java-packages-with-gradle#publishing-packages-to-github-packages</span>
publishing {
    repositories {
        maven {
            name = <span class="hljs-string">"GitHub"</span>
            url = <span class="hljs-string">"https://maven.pkg.github.com/user/project"</span>
            credentials {
                username = System.getenv(<span class="hljs-string">"GITHUB_ACTOR"</span>)
                password = System.getenv(<span class="hljs-string">"GITHUB_TOKEN"</span>)
            }
    }
}
</code></pre>
<p><strong>GITHUB_ACTOR</strong> and <strong>GITHUB_TOKEN</strong> are provided automatically (no configuration required)</p>
<p>In “<a target="_blank" href="https://maven.pkg.github.com/user/project">https://maven.pkg.github.com/user/project</a>” <strong>user</strong> - github user name, <strong>project</strong> - github project name (same as in github project url)</p>
<p>CI script:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">snapshot</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">publish:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">snapshot</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">permissions:</span>
      <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>
      <span class="hljs-attr">packages:</span> <span class="hljs-string">write</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">JDK</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-java@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">distribution:</span> <span class="hljs-string">'zulu'</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-number">17</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">gradle/actions/setup-gradle@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">without</span> <span class="hljs-string">tests</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          chmod +x gradlew
          ./gradlew build -x test -x check --no-daemon
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">GITHUB_TOKEN:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">SNAPSHOT:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">./gradlew</span> <span class="hljs-string">publishAllPublicationsToGitHubRepository</span> <span class="hljs-string">--no-daemon</span>
</code></pre>
<p>Here on each commit project would be built without tests and new snapshot package published.</p>
<h2 id="heading-real-project-with-packages-publication">Real project with packages publication</h2>
<p>In <a target="_blank" href="https://github.com/xvik/dropwizard-guicey/">my project</a>, I use a series of connected workflows:</p>
<ol>
<li><p><a target="_blank" href="https://github.com/xvik/dropwizard-guicey/blob/master/.github/workflows/CI.yml">CI</a> - run and test project</p>
</li>
<li><p>If test success, <a target="_blank" href="https://github.com/xvik/dropwizard-guicey/blob/master/.github/workflows/publish-snapshot.yml">publish</a> new package</p>
</li>
<li><p><a target="_blank" href="https://github.com/xvik/dropwizard-guicey/blob/master/.github/workflows/examples-CI.yml">Run examples project</a> build with just published package</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737270048807/effe6ac7-62e2-42c4-8bb4-96773c92f9c1.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-use-github-package-repo-on-gtihub-actions">Use github package repo on gtihub actions</h2>
<p>On github actions you don’t need an additional token to access githubg packages because <strong>GITHUB_TOKEN</strong> is already provided.</p>
<p><strong>Gradle project</strong></p>
<p>Just add repository, as it was shown in the first chapter.</p>
<pre><code class="lang-java">repositories {
    maven {
        url  = <span class="hljs-string">'https://maven.pkg.github.com/user/project'</span>
        credentials {
            username = findProperty(<span class="hljs-string">'gpr.user'</span>) ?: System.getenv(<span class="hljs-string">"USERNAME"</span>)
            password = findProperty(<span class="hljs-string">'gpr.key'</span>) ?: System.getenv(<span class="hljs-string">"TOKEN"</span>)
        }
    }
}
</code></pre>
<p>This time, token would not be configured in global gradle properties, but, instead, would be provided <strong>in the environment variables</strong>.</p>
<p>Mapping these variables in the <a target="_blank" href="https://github.com/xvik/dropwizard-guicey/blob/master/.github/workflows/examples-CI.yml">CI scipt:</a></p>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">defaults:</span>
      <span class="hljs-attr">run:</span>
        <span class="hljs-attr">working-directory:</span> <span class="hljs-string">examples</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Examples</span> <span class="hljs-string">run</span>
    <span class="hljs-attr">env:</span>
      <span class="hljs-attr">USERNAME:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.actor</span> <span class="hljs-string">}}</span>
      <span class="hljs-attr">TOKEN:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
</code></pre>
<p>Note that</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">defaults:</span>
      <span class="hljs-attr">run:</span>
        <span class="hljs-attr">working-directory:</span> <span class="hljs-string">examples</span>
</code></pre>
<p>Required only in my case, because examples project is in github repository <strong>subfolder</strong> (examples).</p>
<p><strong>Maven project</strong></p>
<p>For maven project, credentials must be inside settings file. Custom settings file could reference environemnt variables <a target="_blank" href="https://github.com/xvik/dropwizard-guicey/blob/master/examples/maven-settings.xml">maven-settings.xml</a>:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">settings</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://maven.apache.org/SETTINGS/1.0.0"</span>
          <span class="hljs-attr">xmlns:xsi</span>=<span class="hljs-string">"http://www.w3.org/2001/XMLSchema-instance"</span>
          <span class="hljs-attr">xsi:schemaLocation</span>=<span class="hljs-string">"http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">activeProfiles</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">activeProfile</span>&gt;</span>github<span class="hljs-tag">&lt;/<span class="hljs-name">activeProfile</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">activeProfiles</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">profiles</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">profile</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>github<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">repositories</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">repository</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>central<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>https://repo1.maven.org/maven2<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">repository</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">repository</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>github<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span>https://maven.pkg.github.com/user/project<span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">snapshots</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">enabled</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">enabled</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">snapshots</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">repository</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">repositories</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">profile</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">profiles</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">servers</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">server</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">id</span>&gt;</span>github<span class="hljs-tag">&lt;/<span class="hljs-name">id</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">username</span>&gt;</span>${env.USERNAME}<span class="hljs-tag">&lt;/<span class="hljs-name">username</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">password</span>&gt;</span>${env.TOKEN}<span class="hljs-tag">&lt;/<span class="hljs-name">password</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">server</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">servers</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">settings</span>&gt;</span>
</code></pre>
<p>In my case, I run maven projects from gradle, using <a target="_blank" href="https://github.com/dkorotych/gradle-maven-exec-plugin">maven-exec-plugin</a> and so could simply <a target="_blank" href="https://github.com/xvik/dropwizard-guicey/blob/master/examples/maven-simple/build.gradle">specify</a> custom settings file:</p>
<pre><code class="lang-java">tasks.register(<span class="hljs-string">'maven-test'</span>, MavenExec) {
    goals <span class="hljs-string">'test'</span>
    options {
        settings = rootProject.file(<span class="hljs-string">'maven-settings.xml'</span>)
    }
}
</code></pre>
<p>But for pure maven project, you could simply override existing settings.xml with a custom file with an additional CI job step.</p>
<h2 id="heading-gradle-plugin-snapshots-and-github-package">Gradle plugin snapshots and github package</h2>
<p>If you develop gradle plugin, you know that it requires two publications:</p>
<ol>
<li><p>Plugin itself</p>
</li>
<li><p>Redirection pom for gradle plugins syntax</p>
</li>
</ol>
<p>Publication method above will correctly publish both into github packages repository.</p>
<p>In order to use such snapshot, repositorty must be specified in <strong>settings.gradle</strong>’s <strong>pluginManagement</strong> section:</p>
<pre><code class="lang-java">pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
        maven {
            url  = <span class="hljs-string">'https://maven.pkg.github.com/user/project'</span>
            credentials {
                username = settings.ext.find(<span class="hljs-string">'gpr.user'</span>) ?: System.getenv(<span class="hljs-string">"USERNAME"</span>)
                password = settings.ext.find(<span class="hljs-string">'gpr.key'</span>) ?: System.getenv(<span class="hljs-string">"TOKEN"</span>)
            }
        }
    }
}
</code></pre>
<p>For <strong>kotlin</strong> syntax (<strong>settings.gradle.kts</strong>):</p>
<pre><code class="lang-java">pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
        maven {
            url  = uri(<span class="hljs-string">"https://maven.pkg.github.com/user/project"</span>)
            credentials {
                username = extra.has(<span class="hljs-string">"gpr.user"</span>).let { <span class="hljs-keyword">if</span> (it) extra[<span class="hljs-string">"gpr.user"</span>] as String <span class="hljs-keyword">else</span> System.getenv(<span class="hljs-string">"USERNAME"</span>)}
                password = extra.has(<span class="hljs-string">"gpr.key"</span>).let { <span class="hljs-keyword">if</span> (it) extra[<span class="hljs-string">"gpr.key"</span>] as String <span class="hljs-keyword">else</span> System.getenv(<span class="hljs-string">"TOKEN"</span>)}
            }
        }
    }
}
</code></pre>
<p><a target="_blank" href="https://github.com/xvik/gradle-animalsniffer-plugin">Example project</a> with “test - publish - run tests” cyle for gradle plugin</p>
]]></content:encoded></item><item><title><![CDATA[Tuning gradle variant compatibility]]></title><description><![CDATA[A real-life case how to force gradle accepting dependency with higer java version requirement.
My animalsniffer gradle plugin targets java 8 (keeps minimum compatiblity):
java {
    sourceCompatibility = 1.8
}

Now, in order to add android support, I...]]></description><link>https://blog.vyarus.ru/tuning-gradle-variant-compatibility</link><guid isPermaLink="true">https://blog.vyarus.ru/tuning-gradle-variant-compatibility</guid><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Mon, 09 Dec 2024 12:23:49 GMT</pubDate><content:encoded><![CDATA[<p>A real-life case how to force gradle accepting dependency with higer java version requirement.</p>
<p><a target="_blank" href="https://github.com/xvik/gradle-animalsniffer-plugin">My animalsniffer gradle plugin</a> targets java 8 (keeps minimum compatiblity):</p>
<pre><code class="lang-java">java {
    sourceCompatibility = <span class="hljs-number">1.8</span>
}
</code></pre>
<p>Now, in order to add android support, I want to add an optional dependency for android plugin:</p>
<pre><code class="lang-java">dependencies {
    compileOnly <span class="hljs-string">'com.android.tools.build:gradle:8.4.0'</span>
}
</code></pre>
<p>But, this will not work:</p>
<pre><code class="lang-plaintext">&gt; Could not resolve all files for configuration ':compileClasspath'.
   &gt; Could not resolve com.android.tools.build:gradle:8.4.0.
     Required by:
         project :
      &gt; No matching variant of com.android.tools.build:gradle:8.4.0 was found. The consumer was configured to find a library for use during compile-time, compatible with Java 8, preferably not packaged as a jar, preferably optimized for standard JVMs, and its dependencies declared externally, as well as attribute 'org.gradle.plugin.api-version' with value '8.6' but:
          - Variant 'apiElements' capability com.android.tools.build:gradle:8.4.0 declares a library for use during compile-time, packaged as a jar, preferably optimized for standard JVMs, and its dependencies declared externally:
              - Incompatible because this component declares a component, compatible with Java 11 and the consumer needed a component, compatible with Java 8
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.6')
          - Variant 'javadocElements' capability com.android.tools.build:gradle:8.4.0 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
                  - Doesn't say anything about its target Java version (required compatibility with Java 8)
                  - Doesn't say anything about its elements (required them preferably not packaged as a jar)
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.6')
          - Variant 'runtimeElements' capability com.android.tools.build:gradle:8.4.0 declares a library for use during runtime, packaged as a jar, preferably optimized for standard JVMs, and its dependencies declared externally:
              - Incompatible because this component declares a component, compatible with Java 11 and the consumer needed a component, compatible with Java 8
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.6')
          - Variant 'sourcesElements' capability com.android.tools.build:gradle:8.4.0 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
                  - Doesn't say anything about its target Java version (required compatibility with Java 8)
                  - Doesn't say anything about its elements (required them preferably not packaged as a jar)
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.6')
</code></pre>
<p>The main problem:</p>
<p><code>Incompatible because this component declares a component, compatible with Java 11 and the consumer needed a component, compatible with Java 8</code></p>
<p>No matter what JDK I actually use for this build (17), gradle complains that dependency is not compatible with declared <em>targetCompatibility</em>\=1.8 (not a mistake, by default <em>targetCompatibility</em> == <em>sourceCompatibility</em>)</p>
<p>But, I assume this dependency to be an optional: android project would use java 11 (17) in any case, but my plugin should also work for pure java projects under java 8.</p>
<p>To workaround this moment, I have to manually declare (override) target jvm attribute for configuration (in build.gradle):</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> org.gradle.api.attributes.java.TargetJvmVersion

configurations.compileClasspath.attributes
    .attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, <span class="hljs-number">11</span>)
</code></pre>
<p>And now java 8 target does not cause problem anymore<em>.</em></p>
<p>Helpful links:</p>
<ol>
<li><p><a target="_blank" href="https://docs.gradle.org/current/userguide/variant_attributes.html">Gradle attributes doc</a></p>
</li>
<li><p><a target="_blank" href="https://docs.gradle.org/current/userguide/variant_aware_resolution.html#2_create_a_custom_outgoing_configuration">Attributes definition example</a></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Releasing GraalVM-generated native binaries with Github Actions (and Gradle)]]></title><description><![CDATA[Recently, I have to update binary release github action for my yaml-updater project and finally added debug and re-run abilities. So I decided to describe it here as it could be easilly reused for other projects (should be a good bootstrap for anyone...]]></description><link>https://blog.vyarus.ru/releasing-graalvm-generated-native-binaries-with-github-actions-and-gradle</link><guid isPermaLink="true">https://blog.vyarus.ru/releasing-graalvm-generated-native-binaries-with-github-actions-and-gradle</guid><category><![CDATA[gradle]]></category><category><![CDATA[GraalVM]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Thu, 17 Oct 2024 11:43:06 GMT</pubDate><content:encoded><![CDATA[<p>Recently, I have to update <a target="_blank" href="https://github.com/xvik/yaml-updater/blob/master/.github/workflows/binaries.yml">binary release</a> github action for my <a target="_blank" href="https://github.com/xvik/yaml-updater">yaml-updater</a> project and finally added debug and re-run abilities. So I decided to describe it here as it could be easilly reused for other projects (should be a good bootstrap for anyone planning to release native binaries).</p>
<h1 id="heading-what-is-binary-release">What is binary release</h1>
<p>First, what I mean by binary release: native binaries generated (with <a target="_blank" href="https://www.graalvm.org/latest/reference-manual/native-image/">graalvm native image</a>) from java application and attached on github release page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729154121054/a6ed96e3-2968-4b24-b680-68eaa3eb5ae1.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p><em>attached jar on screenshot is all-in-one jar (with included dependencies), which is not published to maven central</em></p>
</li>
<li><p>I will explain why linux binary size differ later</p>
</li>
</ol>
<p>It is not possible to build such binary release on local machine because each binary must be build on target os (linux, windows, mac). That’s why github actions required.</p>
<p>Overall release process consists of two parts:</p>
<ol>
<li><p>Release project into maven central and release tag creation (gradle release plugin scope)</p>
</li>
<li><p>Github release creation (in my case manual, but it could be automated too) which trigger binraty action execution. Action attaches created binaries to the just created github release.</p>
</li>
</ol>
<h1 id="heading-native-binary-requirements">Native binary requirements</h1>
<p>What java application could be converted to native binary?<br />In fact, alomost <strong>any java application</strong> (even “hello world”) with the main method (entry point required).</p>
<p>In my case, it was a <a target="_blank" href="https://github.com/xvik/yaml-updater/tree/master/yaml-config-updater-cli">CLI utility</a>, generated with <a target="_blank" href="https://picocli.info/">picocly</a> (ideal candidate for native binary).</p>
<h1 id="heading-project-requirements">Project requirements</h1>
<p>My project use gradle, but it is <em>not important</em> and you can <em>easilly reuse</em> this workflow for maven project (with minimal changes). The most important part is github action logic itself.</p>
<p>There are <a target="_blank" href="https://www.graalvm.org/latest/reference-manual/native-image/#build-a-native-executable-using-maven-or-gradle">maven and gradle plugins</a> maintained by graalvm.</p>
<p>Set up <a target="_blank" href="https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html">gradle plugin</a> (groovy):</p>
<pre><code class="lang-plaintext">plugins {
    id 'org.graalvm.buildtools.native' version '0.10.3'
}

graalvmNative { 
    binaries { 
        main { 
            imageName = project.name 
            mainClass = 'ru.vyarus.yaml.updater.cli.UpdateConfigCli' 
        } 
        all { 
            resources.autodetect() 
        } 
    } 
}
</code></pre>
<p>Plugin needs to know only target main class.</p>
<p>Note that it is possible to avoid plugin usage, but then you’ll have to write native image command manually. Plugin makes life a bit simplier.</p>
<p>IMPORTANT: Plugin requires java 11! In my case, I keep java 8 compatibility and so have to use <a target="_blank" href="https://github.com/xvik/yaml-updater/blob/1.4.4/yaml-config-updater-cli/build.gradle#L3">old plugins syntax</a> and actiavate it ONLY <a target="_blank" href="https://github.com/xvik/yaml-updater/blob/1.4.4/yaml-config-updater-cli/build.gradle#L66">when native compilation is required</a>.</p>
<h1 id="heading-local-test">Local test</h1>
<p>First of all, you may need to install some additional packages: see <a target="_blank" href="https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites">prerequisites</a>.</p>
<p>Then custom JDK is required: see <a target="_blank" href="https://www.graalvm.org/latest/getting-started/#installing">installation</a>. On linux I use sdkman:</p>
<pre><code class="lang-bash">sdk install java 21.0.2-graalce
</code></pre>
<p>You don’t need to set it as default. Just activate it before running your project build:</p>
<pre><code class="lang-bash">sdk use java 21.0.2-graalce 
./gradlew :nativeCompile
</code></pre>
<p>If build successful, your native binary could be found in folder:</p>
<p><code>build/native/nativeCompile</code></p>
<h1 id="heading-github-action">Github action</h1>
<p><a target="_blank" href="https://github.com/xvik/yaml-updater/blob/master/.github/workflows/binaries.yml">My action</a> additionally publish docker image into github repository, but I will not describe this step here - it is very easy to add (just copy this part from my action, if required).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729157126130/753c731b-c4d6-4dbe-946c-0ad4f095ff44.png" alt class="image--center mx-auto" /></p>
<p>Workflow:</p>
<ol>
<li><p>Step selects target tag (will describe later)</p>
</li>
<li><p>3 steps build 3 binaries on 3 OS and upload them as action artifacts</p>
</li>
<li><p>Final step attach binaries to github release</p>
</li>
</ol>
<p>OS-specific steps did not publish directly on github page for proper debug and consistency:</p>
<ol>
<li><p>In case of debug run, final publish step simply not called (but all artifacts attached to action and so could be exemined)</p>
</li>
<li><p>Last step will not run if any OS-specific step will fail (all or nothing)</p>
</li>
</ol>
<h2 id="heading-code">Code</h2>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">native</span> <span class="hljs-string">binaries</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-comment"># for manual debug run (and optional re-release)</span>
  <span class="hljs-attr">workflow_dispatch:</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-comment"># when set, binaries would be uploaded to provided release tag (re-release)</span>
      <span class="hljs-attr">tag:</span>
        <span class="hljs-attr">description:</span> <span class="hljs-string">'Target tag (leave empty for test build)'</span>
        <span class="hljs-attr">required:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">default:</span> <span class="hljs-string">''</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
  <span class="hljs-attr">release:</span>
    <span class="hljs-attr">types:</span> [<span class="hljs-string">published</span>]

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">selectTag:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Select</span> <span class="hljs-string">target</span> <span class="hljs-string">tag</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">outputs:</span>
      <span class="hljs-attr">TAG_NAME:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.select.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">id:</span> <span class="hljs-string">select</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">Select</span> <span class="hljs-string">tag</span> <span class="hljs-string">name</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          if [ -n "${{ inputs.tag }}" ]; then
            tagName=${{ inputs.tag }}
          else
            tagName=${{ github.event.release.tag_name }}
          fi
          echo "Selected tag: $tagName"
          echo "TAG_NAME=$tagName" &gt;&gt; $GITHUB_OUTPUT          
</span>

  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">fail-fast:</span> <span class="hljs-literal">false</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">include:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">artifact:</span> <span class="hljs-string">yaml-updater.exe</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">windows-2022</span>
            <span class="hljs-attr">ext:</span> <span class="hljs-string">.exe</span>
            <span class="hljs-comment"># due to windows defender false detections</span>
            <span class="hljs-attr">upx:</span> <span class="hljs-literal">false</span>

          <span class="hljs-bullet">-</span> <span class="hljs-attr">artifact:</span> <span class="hljs-string">yaml-updater-mac-amd64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">macos-latest</span>
            <span class="hljs-comment"># https://github.com/upx/upx/issues/612</span>
            <span class="hljs-attr">upx:</span> <span class="hljs-literal">false</span>

          <span class="hljs-bullet">-</span> <span class="hljs-attr">artifact:</span> <span class="hljs-string">yaml-updater-linux-amd64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">ubuntu-latest</span>
            <span class="hljs-attr">upx:</span> <span class="hljs-literal">true</span>
            <span class="hljs-attr">shadowJar:</span> <span class="hljs-literal">true</span>

    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.os</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">continue-on-error:</span> <span class="hljs-string">${{</span> <span class="hljs-literal">false</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">needs:</span> [<span class="hljs-string">selectTag</span>]
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo "Selected tag: ${{ needs.selectTag.outputs.TAG_NAME }}"
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">ref:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">graalvm/setup-graalvm@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-string">'22'</span>
          <span class="hljs-attr">distribution:</span> <span class="hljs-string">'liberica'</span>
          <span class="hljs-attr">github-token:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">cache:</span> <span class="hljs-string">gradle</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Verify</span> <span class="hljs-string">GraalVM</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo "GRAALVM_HOME: $GRAALVM_HOME"
          echo "JAVA_HOME: $JAVA_HOME"
          java --version
          native-image --version
</span>
        <span class="hljs-comment"># cache</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">gradle/actions/setup-gradle@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">build</span>
        <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          if [ -n "${{ matrix.shadowJar }}" ]; then
            BUILD_TARGET=":yaml-config-updater-cli:shadowJar :yaml-config-updater-cli:nativeCompile"
          else
            BUILD_TARGET=":yaml-config-updater-cli:nativeCompile"
          fi
          chmod +x gradlew
          ./gradlew ${BUILD_TARGET} --no-daemon
          ## Rename files
          mkdir upload
          cp yaml-config-updater-cli/build/native/nativeCompile/yaml-config-updater-cli${{ matrix.ext }} upload/${{ matrix.artifact }}
          echo "binary=upload/${{ matrix.artifact }}" &gt;&gt; $GITHUB_OUTPUT
          if [ -n "${{ matrix.shadowJar }}" ]; then
            cp yaml-config-updater-cli/build/libs/*-all.jar upload/yaml-updater.jar
            echo "jarFile=upload/yaml-updater.jar" &gt;&gt; $GITHUB_OUTPUT
          fi
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">UPX</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">svenstaro/upx-action@v2</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.upx</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">continue-on-error:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">file:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.build.outputs.binary</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">args:</span> <span class="hljs-string">"-9"</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.build.outputs.binary</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">JAR</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v4</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">matrix.shadowJar</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">yaml-updater.jar</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.build.outputs.jarFile</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Quick</span> <span class="hljs-string">Test</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">./${{</span> <span class="hljs-string">steps.build.outputs.binary</span> <span class="hljs-string">}}</span> <span class="hljs-string">--version</span>


  <span class="hljs-attr">publish:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">fail-fast:</span> <span class="hljs-literal">false</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">artifact:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater.exe</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater-mac-amd64</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater-linux-amd64</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater.jar</span>
    <span class="hljs-attr">needs:</span> [<span class="hljs-string">build</span>, <span class="hljs-string">selectTag</span>]
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">mkdir</span> <span class="hljs-string">-p</span> <span class="hljs-string">tmp</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Download</span> <span class="hljs-string">artifact</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/download-artifact@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">tmp</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">svenstaro/upload-release-action@v2</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">repo_token:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">file:</span> <span class="hljs-string">tmp/${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">tag:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">overwrite:</span> <span class="hljs-literal">true</span>
</code></pre>
<h2 id="heading-run-modes">Run modes</h2>
<p>As I mention before, action will run as soon as new github release would be published (manually or automatically).</p>
<pre><code class="lang-yaml"><span class="hljs-attr">on:</span>
  <span class="hljs-string">.....</span>
  <span class="hljs-attr">release:</span>
    <span class="hljs-attr">types:</span> [<span class="hljs-string">published</span>]
</code></pre>
<p>There are also 2 manual executions:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">on:</span>
  <span class="hljs-comment"># for manual debug run (and optional re-release)</span>
  <span class="hljs-attr">workflow_dispatch:</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-comment"># when set, binaries would be uploaded to provided release tag (re-release)</span>
      <span class="hljs-attr">tag:</span>
        <span class="hljs-attr">description:</span> <span class="hljs-string">'Target tag (leave empty for test build)'</span>
        <span class="hljs-attr">required:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">default:</span> <span class="hljs-string">''</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729157937262/1f33f4e7-c7c8-45a3-bc7c-beadd6177df6.png" alt class="image--center mx-auto" /></p>
<p>If “target tag” input would be empty - debug execution would be started. It will only run 3 native builds without final step. Build artifacts would be available on action build summary page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729158059529/752d609e-3f8a-4298-839b-b9bbfb7840fc.png" alt class="image--center mx-auto" /></p>
<p>(showing build with error on purpose - it was one of many debug runs).</p>
<p>Pay attention that attached files are zipped here: so it is not the real size! On release page files would be attached as-is.</p>
<p>The third run option is manual re-release. By default, github action would use the same commit as current action itself, so you can’t fix action yaml and re-perform release on tag.</p>
<p>The third mode exactly workarounds this default behaviour: you can run actual github action, but build and release sources from exact tag (and binaries would be attached to an appropriate release page).</p>
<h2 id="heading-tag-selection-job">Tag selection job</h2>
<p>In order to make all this work, additional (first) step is required:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">selectTag:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Select</span> <span class="hljs-string">target</span> <span class="hljs-string">tag</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">outputs:</span>
      <span class="hljs-attr">TAG_NAME:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.select.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">id:</span> <span class="hljs-string">select</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">Select</span> <span class="hljs-string">tag</span> <span class="hljs-string">name</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          if [ -n "${{ inputs.tag }}" ]; then
            tagName=${{ inputs.tag }}
          else
            tagName=${{ github.event.release.tag_name }}
          fi
          echo "Selected tag: $tagName"
          echo "TAG_NAME=$tagName" &gt;&gt; $GITHUB_OUTPUT</span>
</code></pre>
<p>It will look if tag name provided as input or it was a release event and will take release tag from event. In case of manual debug run (without tag name), tag name will remain empty.</p>
<p>Result is written into declared job output:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">outputs:</span>
      <span class="hljs-attr">TAG_NAME:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.select.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
    <span class="hljs-string">....</span>
    <span class="hljs-string">echo</span> <span class="hljs-string">"TAG_NAME=$tagName"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">$GITHUB_OUTPUT</span>
</code></pre>
<p>Other jobs declare dependency on first job and so could reference its output values:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">needs:</span> [<span class="hljs-string">selectTag</span>]
<span class="hljs-string">....</span>
<span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
</code></pre>
<p>In the above example, “if” would prevent job execution when TAG_NAME not set (in debug mode).</p>
<h2 id="heading-binary-build-jobs">Binary build jobs</h2>
<p>The main build use matrix to run on three different OS:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">fail-fast:</span> <span class="hljs-literal">false</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">include:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">artifact:</span> <span class="hljs-string">yaml-updater.exe</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">windows-2022</span>
            <span class="hljs-attr">ext:</span> <span class="hljs-string">.exe</span>
            <span class="hljs-comment"># due to windows defender false detections</span>
            <span class="hljs-attr">upx:</span> <span class="hljs-literal">false</span>

          <span class="hljs-bullet">-</span> <span class="hljs-attr">artifact:</span> <span class="hljs-string">yaml-updater-mac-amd64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">macos-latest</span>
            <span class="hljs-comment"># https://github.com/upx/upx/issues/612</span>
            <span class="hljs-attr">upx:</span> <span class="hljs-literal">false</span>

          <span class="hljs-bullet">-</span> <span class="hljs-attr">artifact:</span> <span class="hljs-string">yaml-updater-linux-amd64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">ubuntu-latest</span>
            <span class="hljs-attr">upx:</span> <span class="hljs-literal">true</span>
            <span class="hljs-attr">shadowJar:</span> <span class="hljs-literal">true</span>
</code></pre>
<h3 id="heading-checkout">Checkout</h3>
<p>First step would checkout correct version:</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo "Selected tag: ${{ needs.selectTag.outputs.TAG_NAME }}"
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">ref:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
</code></pre>
<p>Note that in case of debug build (manual run without tag name), TAG_NAME would be empty and so action would checkout current branch head (the same as if ref option would not be declared at all). But in case of manual run with tag, it would checkout tag sources (which is important for re-releasing binaries).</p>
<h3 id="heading-setup-graal-and-gradle">Setup graal and gradle</h3>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">graalvm/setup-graalvm@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-string">'22'</span>
          <span class="hljs-attr">distribution:</span> <span class="hljs-string">'liberica'</span>
          <span class="hljs-attr">github-token:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">cache:</span> <span class="hljs-string">gradle</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Verify</span> <span class="hljs-string">GraalVM</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo "GRAALVM_HOME: $GRAALVM_HOME"
          echo "JAVA_HOME: $JAVA_HOME"
          java --version
          native-image --version
</span>
        <span class="hljs-comment"># cache</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">gradle/actions/setup-gradle@v3</span>
</code></pre>
<p>Graalvm setup action is also <a target="_blank" href="https://github.com/graalvm/setup-graalvm">provided by graalvm team</a> and that’s wonderful! Before, you have to <a target="_blank" href="https://github.com/xvik/yaml-updater/blob/1.4.2/.github/workflows/binaries.yml#L38">manually install additional pre-requisites</a> on windows and now its just a one simple step!</p>
<p>See action docs for <a target="_blank" href="https://github.com/graalvm/setup-graalvm?tab=readme-ov-file#options">options description</a>.</p>
<h3 id="heading-build-itself">Build itself</h3>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">build</span>
        <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          if [ -n "${{ matrix.shadowJar }}" ]; then
            BUILD_TARGET=":yaml-config-updater-cli:shadowJar :yaml-config-updater-cli:nativeCompile"
          else
            BUILD_TARGET=":yaml-config-updater-cli:nativeCompile"
          fi
          chmod +x gradlew
          ./gradlew ${BUILD_TARGET} --no-daemon
          ## Rename files
          mkdir upload
          cp yaml-config-updater-cli/build/native/nativeCompile/yaml-config-updater-cli${{ matrix.ext }} upload/${{ matrix.artifact }}
          echo "binary=upload/${{ matrix.artifact }}" &gt;&gt; $GITHUB_OUTPUT
          if [ -n "${{ matrix.shadowJar }}" ]; then
            cp yaml-config-updater-cli/build/libs/*-all.jar upload/yaml-updater.jar
            echo "jarFile=upload/yaml-updater.jar" &gt;&gt; $GITHUB_OUTPUT
          fi</span>
</code></pre>
<p>This is the only step that depends on build tool.</p>
<p>As my application requires 3rd party jars, I want to also build an all-in-one jar (which is simpier to use, in case if binary can’t be used). There is an <strong>shadowJar</strong> matrix option to build complete jar only once on linux (and so on linux <strong>:shadowJar</strong> task must be called to build it).</p>
<p>Next, copying generated files into <em>upload</em> folder in order to properly re-name generated files (and simplify paths).</p>
<p>Note that undeclared outputs declared here:</p>
<pre><code class="lang-yaml">        <span class="hljs-string">echo</span> <span class="hljs-string">"binary=upload/$<span class="hljs-template-variable">{{ matrix.artifact }}</span>"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">$GITHUB_OUTPUT</span>
         <span class="hljs-string">echo</span> <span class="hljs-string">"jarFile=upload/yaml-updater.jar"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">$GITHUB_OUTPUT</span>
</code></pre>
<p>To reference them in further steps as: <code>${{ steps.build.outputs.binary }}</code></p>
<h3 id="heading-upx">UPX</h3>
<p>As you may note, resulted binaries are quite big (~20mb in my case). It is possible to shrink it with graalvm but with a lot of efforts (mainly by getting rid of reflection), but it is not possible in my case.</p>
<p>Instead, <a target="_blank" href="https://github.com/upx/upx">upx</a> tool could shrink binary size (in my case from 20mb into 6mb).</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">UPX</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">svenstaro/upx-action@v2</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.upx</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">continue-on-error:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">file:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.build.outputs.binary</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">args:</span> <span class="hljs-string">"-9"</span>
</code></pre>
<p>But, currently upx is <a target="_blank" href="https://github.com/upx/upx/issues/612">not compatible with mac</a> (homebrew would even not allow you to install it).</p>
<p>On windows, compressed binaries are quite often being detected as infected by windows defender. It is strange, because in theory any antivirus could unupx (upx -d binary) it and check correctly. I didn’t find a way to workaround this.</p>
<p>So upx would be applied only for linux (and that’s why linux binary on screen at page head is the smallest one).</p>
<pre><code class="lang-yaml"><span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.upx</span> <span class="hljs-string">}}</span>
</code></pre>
<h3 id="heading-store-generated-binary-as-action-artifact">Store generated binary as action artifact</h3>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.build.outputs.binary</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">JAR</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v4</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">matrix.shadowJar</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">yaml-updater.jar</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.build.outputs.jarFile</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Quick</span> <span class="hljs-string">Test</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">./${{</span> <span class="hljs-string">steps.build.outputs.binary</span> <span class="hljs-string">}}</span> <span class="hljs-string">--version</span>
</code></pre>
<p>This is important for debug run, because we could download generated binary and test/investigate it locally. Also, uploading all-in-one jar (generated only on linux).</p>
<p>Quick test is required just to reveal potentially incorrect binary. For example, on mac, it detected upx-compressed binary incompatibility. It is important to test after upload to be able to download and check binary after error.</p>
<h2 id="heading-publish-job">Publish job</h2>
<pre><code class="lang-yaml"><span class="hljs-attr">publish:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">fail-fast:</span> <span class="hljs-literal">false</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">artifact:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater.exe</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater-mac-amd64</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater-linux-amd64</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">yaml-updater.jar</span>
    <span class="hljs-attr">needs:</span> [<span class="hljs-string">build</span>, <span class="hljs-string">selectTag</span>]
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">mkdir</span> <span class="hljs-string">-p</span> <span class="hljs-string">tmp</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Download</span> <span class="hljs-string">artifact</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/download-artifact@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">tmp</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">svenstaro/upload-release-action@v2</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">repo_token:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">file:</span> <span class="hljs-string">tmp/${{</span> <span class="hljs-string">matrix.artifact</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">tag:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">overwrite:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>Publish job starts only if target tag declared (release event or manual re-release):</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">needs.selectTag.outputs.TAG_NAME</span> <span class="hljs-string">}}</span>
</code></pre>
<p>And if previous steps were successfull:</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">needs:</span> [<span class="hljs-string">build</span>, <span class="hljs-string">selectTag</span>]
</code></pre>
<p>Then downloading artifacts from action artifacts storage (uploaded by build jobs) and attaching them to github release.</p>
]]></content:encoded></item><item><title><![CDATA[Gradle cross-project configuration side effects]]></title><description><![CDATA[I was always using gradle allprojects and suprojects sections as a very handy way for multi-module projects configuration.
But, it appears, that using these shortcuts is officially a bad practice.
Quote from gradle forum thread:

Consider switching f...]]></description><link>https://blog.vyarus.ru/gradle-cross-project-configuration-side-effects</link><guid isPermaLink="true">https://blog.vyarus.ru/gradle-cross-project-configuration-side-effects</guid><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Tue, 12 Mar 2024 11:37:26 GMT</pubDate><content:encoded><![CDATA[<p>I was always using gradle <code>allprojects</code> and <code>suprojects</code> sections as a very handy way for multi-module projects configuration.</p>
<p>But, it appears, that using these shortcuts is <a target="_blank" href="https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins_vs_cross_configuration">officially a bad practice</a>.</p>
<p>Quote from <a target="_blank" href="https://discuss.gradle.org/t/java-toolchain-present-in-allprojects-block-is-no-applied-to-subproject-unless-apply-plugin-java-is-in-the-block/46938/2">gradle forum thread</a>:</p>
<ol>
<li><p>Consider switching from Groovy DSL to Kotlin DSL. This is by now the recommended default DSL, it immediately gives you type-safe build scripts, it gives you actually helpful error messages if you mess up the syntax, and you get amazingly better IDE support when using a proper IDE like IntelliJ IDEA.</p>
</li>
<li><p>Do not use <code>allprojects { ... }</code>, <code>subprojects { ... }</code>, <code>project(...) { ... }</code> or similar. Those are bad practice and immediately introduce project coupling by doing cross-project configuration. This disturbs more sophisticated Gradle features and even prevents some upcoming features to work properly. Instead you should use <a target="_blank" href="https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html">convention plugins</a>, for example in <code>buildSrc</code> or and <a target="_blank" href="https://docs.gradle.org/current/userguide/composite_builds.html">included build</a>, for example implemented as <a target="_blank" href="https://docs.gradle.org/current/userguide/custom_plugins.html#sec:precompiled_plugins">precompiled script plugins</a>. Those you can then targetedly apply to the projects where their effect should be present, so that you for example only apply Java conventions to projects that are actually Java projects.</p>
</li>
</ol>
<p><strong>Disclaimer</strong>: I put this on top to point to official incorectness of such way of configuration. Still, I consider it as the most understandable and easy to use.</p>
<p>Below you'll find one of hard to debug problems appeared after configuring common staff in the root project with groovy DSL (which, eventually, lead me to the above insight).</p>
<h3 id="heading-the-problem">The problem</h3>
<p>My problem appeared with pom configuration (<a target="_blank" href="https://docs.gradle.org/current/userguide/composite_builds.html">using my po</a><a target="_blank" href="https://github.com/xvik/gradle-pom-plugin">m plugin</a>) inside multi-module project: as all modules share the same core pom information (developer, license, scm etc.), it would be logical to apply it in <code>allprojects</code> section:</p>
<pre><code class="lang-java">plugins {
    id <span class="hljs-string">'java-platform'</span>
    id <span class="hljs-string">'ru.vyarus.pom'</span>
}

allprojects {
    maven.pom {
          developers {
              developer {
                  id = <span class="hljs-string">'johnd'</span>
                  name = <span class="hljs-string">'John Doe'</span>
                  email = <span class="hljs-string">'johnd@somemail.com'</span>
              }
         }
    }
}

subprojects {
    apply plugin: <span class="hljs-string">'java'</span>
    apply plugin: <span class="hljs-string">'ru.vyarus.pom'</span>
}
</code></pre>
<p>Root project is a BOM, but I omit <a target="_blank" href="https://github.com/xvik/dropwizard-guicey/blob/master/build.gradle">related configurations</a> to clearly show the problem.</p>
<p>This should put developer section in all poms, including root BOM (allprojects). But java plugin must be applied only for sub-modules because root project does not have sources. Looks logical.</p>
<p>But actual behaviour would be: BOM (root pom) would contain <em>many duplicated</em> developer tags and sub-modules would not contain any!</p>
<p><strong>What happend</strong>: When allprojects is applied for sub-module, <code>ru.vyarus.pom</code> plugin <em>is not applied yet</em> and gradle goes to the root project, trying to find referenced extension (<code>maven</code>), and successfully finds it (so root pom being configured multiple times with the same chunk).</p>
<p>If we try to move <code>subprojects</code> section above the <code>allprojects</code> - it would work as planned. But it might be much harder to spot in the real build file.</p>
<p>Note that simply moving pom plugin activation (<code>apply plugin: 'ru.vyarus.pom'</code>) into <code>allprojects</code> section would not work, becuase plugin is activated only after <code>java</code> (or <code>java-platform</code>) plugin activation.</p>
<h3 id="heading-debugging">Debugging</h3>
<p><a target="_blank" href="https://discuss.gradle.org/t/java-toolchain-present-in-allprojects-block-is-no-applied-to-subproject-unless-apply-plugin-java-is-in-the-block/46938/2">Mentioned gradle forum topic</a> provides a nice way to debug such things:</p>
<pre><code class="lang-java">println(<span class="hljs-string">"ROOT: ${System.identityHashCode(maven)}"</span>)

allprojects {
    println(<span class="hljs-string">"$name: ${System.identityHashCode(maven)}"</span>)
    maven.pom {
        ...
    }
}
</code></pre>
<p>Here we print identity of the <code>maven</code> extension object, which would be equal if root project extension is used in sub-modules.</p>
<p>I didn't use it in my case (becuase pom plugin supports debug mode, showing all xml modifications), but need to mention it as a very nice alternative.</p>
<h3 id="heading-solution">Solution</h3>
<p>Suppose we can't move subprojects, then the simplest solution would be in delaying this configuration (afterEvaluate usage was highly <a target="_blank" href="https://discuss.gradle.org/t/java-toolchain-present-in-allprojects-block-is-no-applied-to-subproject-unless-apply-plugin-java-is-in-the-block/46938/2">not recommended in thread</a>):</p>
<pre><code class="lang-java">allprojects {
    afterEvaluate {
        maven.pom {
            ...
        }
    }
}
</code></pre>
<p>But, it might not be enoght if extension values are used in <code>afterEvaluate</code> in plugin, which is a common practice (and so your <code>afterEvaluate</code> block would execute after extension values were used, and would be ignored).</p>
<p>The best way would be to wait for required (or related) plugin activation:</p>
<pre><code class="lang-java">allprojects {
    plugins.withId(<span class="hljs-string">'java'</span>) {
        maven.pom {
            ...
        }
    }
}
</code></pre>
<p>(or <code>pluginManager.withPlugin("java-base")</code> as suggested in thread, but this version is shorter).</p>
<p>Side note: when groovy closure was used in pom plugin for xml configuration, it was possible to <a target="_blank" href="https://github.com/xvik/gradle-pom-plugin/commit/1dfc9182a5d0362a98b2f245554125e61adebaf0#diff-50333754d608fec9d17f0799a0a0558959ff3783c08d98dcf3b523c73b01b384L124">resolve closure declaration project</a> and detect such mis-use automatically. But, unfortunately, it is impossible with pure <code>Action</code> or direct model object access.</p>
]]></content:encoded></item><item><title><![CDATA[Intercepting HTTPS traffic between servers]]></title><description><![CDATA[It is a common need to intercept traffic between servers. For example, last time I need this to verify keycloak logout request correctness (sent by my application).
Mitmproxy is ideal for such things. It's a small and handy reverse proxy, but with ab...]]></description><link>https://blog.vyarus.ru/intercepting-https-traffic-between-servers</link><guid isPermaLink="true">https://blog.vyarus.ru/intercepting-https-traffic-between-servers</guid><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Fri, 20 Oct 2023 11:16:41 GMT</pubDate><content:encoded><![CDATA[<p>It is a common need to intercept traffic between servers. For example, last time I need this to verify keycloak logout request correctness (sent by my application).</p>
<p><a target="_blank" href="https://mitmproxy.org/">Mitmproxy</a> is ideal for such things. It's a small and handy reverse proxy, but with ability to <strong>modify</strong> requests and responses and supporting https.</p>
<h2 id="heading-usage">Usage</h2>
<p><code>mitmproxy --mode reverse:https://some.url</code></p>
<p>This would start proxy UI on port 8080. So <code>https://localhost:8080/</code> calls would be redirected into <code>https://some.url</code> (and so in application configuration proxy url must be used instead of direct url)</p>
<p>To use custom port:</p>
<p><code>mitmproxy --mode reverse:https://some.url -p 4000</code></p>
<p>Now <code>https://localhost:4000/</code> would lead to <code>https://some.url</code></p>
<h2 id="heading-ui">UI</h2>
<p>Root screen shows all intercepted requests:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697798117891/5c38ef9b-bc68-42f9-91f4-5180bd2d4af1.png" alt class="image--center mx-auto" /></p>
<p>You'll need to use keyboard:</p>
<ul>
<li><p>Arrows (up/down) - select request</p>
</li>
<li><p>Enter - open request info</p>
</li>
<li><p>q - back (from request info or any other screen; remember!)</p>
</li>
<li><p>Shift+O - options (useful to modify options on started instance instead of changing parameters)</p>
</li>
</ul>
<p>All other keys could be seen in the bottom bar.</p>
<p>GET request details example (after enter hit on any request line) :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697798378549/4380f536-43f6-4da6-8228-68ec8247b6e1.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-response-modification">Response modification</h2>
<p>In my (keycloak) case it was important to rewrite original url in configuration response into proxy url.</p>
<p>You can do it with <a target="_blank" href="https://docs.mitmproxy.org/dev/overview-features/#modify-body">modify_body</a> option:</p>
<p><code>mitmproxy --mode reverse:https://some.url -p 4000 --modify-body "#https://keycloak-url#https://localhost:4000"</code></p>
<p>Important moment: first symbol of <code>--modify-body</code> value declares parts separator! In this example <code>#</code> used as separation symbol. <code>https://keycloak-url</code> would be replaced with <code>https://localhost:4000</code> in all responses (first part is a regexp, but in simple cases, just strings would work).</p>
<p>Note that you can always modify this value through options (<code>shift+O</code>): just find <code>modify_body</code> option and hit <code>enter</code> 2 times to get into edit mode. After edition <code>esc</code> to exit editor and 2 times <code>q</code> to get back to the main screen</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697798996591/42dd6a9c-5510-46b7-a086-dbe3c2209ae4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-proxying-kecloak">Proxying kecloak</h2>
<p>It would not be helpful for anyone, but just to remember for me. Proxied keycloak would not produce valid tokens, becuase they would be issued with a "wrong" host. In order to overcome this, keycloak must be configured with proxy url as frontend:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697799871722/23c2580a-4a85-40dd-95aa-3e69fcc4c4ea.png" alt class="image--center mx-auto" /></p>
<p><strong>WARNIG</strong>: After that <strong>your main keycloak url would stop working</strong> (for the same reason)! So don't forget to clear this value after using proxy (to clear value use proxy url to access keycloak).</p>
<p>And, if you, by mistake, put an http url and your keycloak is under https, then to access keycloak you'll have to allow mixed content in chrome:</p>
<ol>
<li><p>Click the lock (caution) icon, then click Site settings.</p>
</li>
<li><p>Scroll to Insecure content, then use the drop-down list to change “Block (default)” to “Allow.”</p>
</li>
<li><p>Reload the VEC page.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Fix IDEA crashes in Ubuntu  (22.x)]]></title><description><![CDATA[For at least a year I observe sudden crashes of IntelliJ Idea (with SEGFAULT). After the latest ubuntu update, it appear way too often: sometimes even can't open IDE at all becuase it crash during "indexing" phase.
Did not help
I tried the most commo...]]></description><link>https://blog.vyarus.ru/fix-idea-crashes-in-ubuntu-22x</link><guid isPermaLink="true">https://blog.vyarus.ru/fix-idea-crashes-in-ubuntu-22x</guid><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Sat, 21 Jan 2023 10:17:54 GMT</pubDate><content:encoded><![CDATA[<p>For at least a year I observe sudden crashes of IntelliJ Idea (with SEGFAULT). After the latest ubuntu update, it appear way too often: sometimes even can't open IDE at all becuase it crash during "indexing" phase.</p>
<h3 id="heading-did-not-help">Did not help</h3>
<p>I tried the most common advises:</p>
<ul>
<li><p>Remove caches by removing <code>~/.cache/JetBrains/IntelliJIdea2022.3/</code> folder</p>
</li>
<li><p>Disable embedded chrome (for markdown rendering): <code>ide.browser.jcef.enabled=false</code> in idea.properties (Help/Edit custom properties)<br />  After all I enabled it back.</p>
</li>
</ul>
<p>But it didn't help. Moreover, other apps like chrome and docker start failing too.</p>
<h3 id="heading-solution">Solution</h3>
<p>It appears that the problem was in swap file size (even having 64gb of memory, swap is still used). By default, swap file is 2g in ununtu.</p>
<p>To <a target="_blank" href="https://ploi.io/documentation/server/change-swap-size-in-ubuntu">enlarge it into 4gb</a>:</p>
<p><code>swapoff -a</code><br /><code>fallocate -l 4G /swapfile</code><br /><code>chmod 600 /swapfile</code><br /><code>mkswap /swapfile</code><br /><code>swapon /swapfile</code></p>
<p>Another advice was to <a target="_blank" href="https://www.omgubuntu.co.uk/2022/06/ubuntu-22-04-systemd-oom-killing-apps#comment-5884426488">encrease swappiness</a> to avoid going anything into swap:</p>
<p><code>echo vm.swappiness=100 | sudo tee -a /etc/sysctl.conf &amp;&amp; sudo sysctl -p</code></p>
<p>And, the last thing is the new <code>systemd-oomd</code> ubuntu service watching applications for memory and swap consuption and killing them to prevent kernel out of memory.</p>
<p>You can <a target="_blank" href="https://askubuntu.com/a/1409074">see its log</a> with: <code>journalctl -u systemd-oomd</code></p>
<p>As I understand, it works sometimes not as well as it was planned, and so, having enough memory, it makes sense to <a target="_blank" href="https://askubuntu.com/a/1405588">switch it off</a> to avoid redundant checks:</p>
<p><code>systemctl is-enabled systemd-oomd</code><br /><code>systemctl mask systemd-oomd</code></p>
<p>(masking to prevent other services to start it)</p>
<p>I suspect the last step wasn't required, but still did it. Overall, I observe much better system performance. IDEA craches are all gone!</p>
]]></content:encoded></item><item><title><![CDATA[Adding docker support into gradle plugin with testcontainers]]></title><description><![CDATA[Preface
Several years ago I was searching for a simple (but pretty) documentation tool for my open source projects. Eventually, I stopped on mkdocs with material theme:

But, it is a python tool and I need to use it in gradle. I liked the tool so muc...]]></description><link>https://blog.vyarus.ru/adding-docker-support-into-gradle-plugin-with-testcontainers</link><guid isPermaLink="true">https://blog.vyarus.ru/adding-docker-support-into-gradle-plugin-with-testcontainers</guid><category><![CDATA[Java]]></category><category><![CDATA[Docker]]></category><category><![CDATA[gradle]]></category><category><![CDATA[Testcontainers]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Thu, 03 Nov 2022 11:52:52 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-preface">Preface</h1>
<p>Several years ago I was searching for a simple (but pretty) documentation tool for my open source projects. Eventually, I stopped on <a target="_blank" href="https://www.mkdocs.org/">mkdocs</a> with <a target="_blank" href="https://squidfunk.github.io/mkdocs-material/">material theme</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667373515597/bSufM2b6u.png" alt="Screenshot from 2022-11-02 14-18-12.png" /></p>
<p>But, it is a <a target="_blank" href="https://www.python.org/">python</a> tool and I need to use it in <a target="_blank" href="https://gradle.org/">gradle</a>. I liked the tool so much that decided to build a gradle plugin for it. I thought, It would be logical to implement basic python support in a separate plugin, just in case if I would decide to do a plugin for some other python module later. That's how plugins were born:</p>
<ul>
<li><a target="_blank" href="https://github.com/xvik/gradle-use-python-plugin">Use-python plugin</a></li>
<li><a target="_blank" href="https://github.com/xvik/gradle-use-python-plugin">Mkdocs plugin</a></li>
</ul>
<p>(going a bit forward, splitting plugin was a right move - python plugin appears to be even more popular then specialized mkdocs plugin)</p>
<p>The basic idea was to use python the same way as we use java: install it once manually and allow plugin to automate dependencies management. There is a <a target="_blank" href="https://virtualenv.pypa.io/en/latest/">virtualenv</a> module in python, allowing creation of isolated environments where we can install project-specific modules without affecting global python installation (very like global/local npm modules). </p>
<p>Eventually, there appears to be many-many python-related problems with os-specific execution, differences between python versions (and even the way it was installed), but this article is not about it (maybe next time). </p>
<p>Once, I was asked if it is possible to avoid local python installation and use docker container instead. First, I thought "no, no-no-no, no way" and wasn't going to do anything, but, as usual, then I got curious "why not?" (besides, python provides <a target="_blank" href="https://hub.docker.com/_/python">official python images</a>).</p>
<h1 id="heading-why-testcontainers">Why testcontainers</h1>
<p>Initially, I thought to use <a target="_blank" href="https://github.com/docker-java/docker-java">docker-java</a>, but it is a low-level api almost without documentation. To speed up prototyping phase (I wasn't ready for complete plugin rewrite and so must be sure that it is possible to introduce docker in relatively simple way), I used <a target="_blank" href="https://www.testcontainers.org/">testcontainers</a> instead (which, actually, build above docker-java).</p>
<p>If you have never heard about it: testcontainers is a testing library (with junit, spock etc. integrations) providing easy container management in tests (starting fresh database, redis or anything else). I've been using it for several years already and it's a real life saver for integration tests. And that's why I thought about it (and, by the way, I know about mkdocs exactly because testcontainers use mkdocs for <a target="_blank" href="https://www.testcontainers.org/">documentation</a>).</p>
<p>Testcontainers provide really simple api for <a target="_blank" href="https://www.testcontainers.org/features/creating_container/">starting a container</a>:</p>
<pre><code class="lang-java"><span class="hljs-keyword">new</span> GenericContainer(DockerImageName.parse(<span class="hljs-string">"python:3.10.8-alpine3.15"</span>))
    .withCommand(<span class="hljs-string">"python"</span>, <span class="hljs-string">"--verson"</span>)
    .withStartupTimeout(Duration.ofSeconds(<span class="hljs-number">1</span>))
    .start()
</code></pre>
<p>This code will download python image (~40mb) start <code>python --version</code> and exit.</p>
<p>It quickly appears that my <em>need is pretty much the same as in integration test</em>: start docker, do something and make sure it will be stopped after. Docker-java is <a target="_blank" href="https://github.com/docker-java/docker-java/blob/master/docs/getting_started.md">too generic</a> (I would have to implement many things proveded in testcontainers out of the box).</p>
<p>One important aspect is that testcontainers use additional specialized container (ryuk) which would <strong>guarantee proper shutdown for started containers</strong>. This might not be obvious, but stale containers are a pain, especially if they use port bindings and so would prevent consequent runs and require users to do manual investigations and cleanups. In the case of gradle, crashes are possible. Moreover, tasks may run infinite python processes (like mkdocs dev server) which might only be stopped forcefully and without proper container cleanup this would be a problem.</p>
<p>This also implies stateless containers usage: you can't expect container state to be preserved between restarts. Not a big problem in my case, as I can simply store environment inside mapped directory (the same for commands output). From the other side, stateful containers would also become a problem: possible damaged state, requiring additional user actions to flush that state.</p>
<p>A little downside of using testcontainers is that you'll have to keep junit dependency in classpath - testcontainers is a test tool at first hand, and wasn't supposed to be used outside of tests.</p>
<h1 id="heading-container-os">Container OS</h1>
<p>After looking at official <a target="_blank" href="https://hub.docker.com/_/python">python image</a> page and seeing windows-based images (like <code>3.10.8-windowsservercore-1809</code>) I was hoping to be able to run windows containers on linux (naive me). Of course, it's not possible.</p>
<p>In general, you can run linux containers on linux (and macos) and windows containers on windows. But, actually, docker client on windows is installed with <a target="_blank" href="https://github.com/microsoft/WSL2-Linux-Kernel">WSL2</a> support by default and so all linux containers work on windows (of course, linux containers were working before WSL, but with WSL it works on <a target="_blank" href="https://docs.docker.com/desktop/install/windows-install/#system-requirements">all windows versions</a>).</p>
<p>In order to use windows containers, you'll need windows PRO (Home does not support this!) and in the tray docker icon switch to "Use windows containers". Since that moment only windows containers would work.</p>
<p>On linux, windows containers would not work (in theory, it could work with windows virtualization (like windows do to run linux containers), but it will quickly face windows license problem, so not sure it would ever happen).</p>
<p>As you can see, you can <strong>target only linux containers</strong> and it would work everywhere! Windows containers are too exotic.</p>
<p>And, going back to testcontainers, they are <a target="_blank" href="https://www.testcontainers.org/supported_docker_environment/windows/#windows-container-on-windows-wcow">limited to linux containers only</a> because ryuk (special container used for proper shutdown) does not provide windows version and so testcontainers would not be able to run with windows containers (but <a target="_blank" href="https://github.com/testcontainers/testcontainers-java/issues/5621">it might someday</a>).</p>
<p>Looking through the prism of the gradle plugin, a single OS is a plus, because you don't have to support  different commands (everything is obviously different for windows and linux). I added theoretical windows support in my plugin for the future, but not sure this would ever be useful (I know about windows specifics too late).</p>
<h2 id="heading-ci">CI</h2>
<p>Need to mention one testcontainers limitation: they are not working currently on windows CI servers like <a target="_blank" href="https://www.appveyor.com/">Appveyour</a>. The problem is that such servers are based on Windows Servers which is <a target="_blank" href="https://github.com/testcontainers/testcontainers-java/issues/2960">not supported</a> by testcontainers.</p>
<p>The only known compatible windows CI is <a target="_blank" href="https://github.com/testcontainers/testcontainers-java/issues/2960#issuecomment-774487244">azure pipelines</a>. But its free plan is very limited.</p>
<p>On linux CI there are no problems. For example, github actions do not even require any additional configurations.</p>
<h2 id="heading-file-sharing">File sharing</h2>
<p>Another important aspect is file sharing. In case of gradle plugin, I simply mount entire project folder into container (and automatically rewrite python command to match correct paths):</p>
<pre><code class="lang-java"><span class="hljs-keyword">new</span> GenericContainer(DockerImageName.parse(<span class="hljs-string">"python:3.10.8-alpine3.15"</span>))
  .withFileSystemBind(<span class="hljs-string">"/local/path/project-name"</span>, <span class="hljs-string">"/usr/src/project-name"</span>, BindMode.READ_WRITE)
  ...
</code></pre>
<p>But there is a problem: container works with a <em>root user</em> and so <em>creates all files as root</em>. On windows and macos, docker volumes are implemented using network mount and so files created inside container (inside volume) with root user are correctly remapped into current user. </p>
<p>On linux - files created inside container would <em>preserve root</em> and so you'll need to use <code>sudo</code> to do anything with them. As an example, if your command, executed inside a container, would write anything inside the project build directory then <code>gradlew clean</code> would fail to execute (due to not enough rights).</p>
<p>To workaround this problem, I execute <code>chown</code> inside the container just after python execution to correct rights for all created files. Note that target user does not need to exist inside the container when performing chown with uid. </p>
<p>To avoid current user uid resolution, I used uid (and gid) from project root folder (which certainly must be good enough):</p>
<pre><code class="lang-java">Path projectDir = project.rootProject.rootDir.toPath()
<span class="hljs-keyword">int</span> uid = (<span class="hljs-keyword">int</span>) Files.getAttribute(projectDir, <span class="hljs-string">"unix:uid"</span>, LinkOption.NOFOLLOW_LINKS)
<span class="hljs-keyword">int</span> gid = (<span class="hljs-keyword">int</span>) Files.getAttribute(projectDir, <span class="hljs-string">"unix:gid"</span>, LinkOption.NOFOLLOW_LINKS)

dockerExec(<span class="hljs-keyword">new</span> String[]{<span class="hljs-string">"chown"</span>, <span class="hljs-string">"-Rh"</span>, uid+<span class="hljs-string">":"</span>+gid, dir.toAbsolutePath()})
</code></pre>
<p>As python plugin is generic, I have to add public <code>dockerChown(path)</code> method directly to python task, so it would be possible to use it in <code>doLast</code> hook:</p>
<pre><code class="lang-groovy">task sample(type: PythonTask) {
    command = '-c "with open(\'build/temp.txt\', \'w+\') as f: pass"'
    doLast {
        dockerChown 'build/temp.txt'
    }
}
</code></pre>
<p>Also note that on windows operations on mounted files would perform slower. This is a <a target="_blank" href="https://github.com/microsoft/WSL/issues/4197">known WSL2 limitation</a>. It is not a big problem for a few files used, but a serious problem for hundreds or thousands of files.
If you have windows PRO, you can try to disable WSL2 support in settings, switching to pure Hyper-V which should be much better.</p>
<h1 id="heading-execution">Execution</h1>
<p>There are two possible ways of command execution. </p>
<h2 id="heading-container-command">Container command</h2>
<p>Command could be specified as container command:</p>
<pre><code class="lang-java"><span class="hljs-keyword">try</span> {
  GenericContainer container = <span class="hljs-keyword">new</span> GenericContainer(...)
      .withStartupTimeout(Duration.ofSeconds(<span class="hljs-number">1</span>))
      .withCommand(<span class="hljs-string">"python"</span>, <span class="hljs-string">"--verson"</span>)
      .withLogConsumer(...)   
      .start()
} <span class="hljs-keyword">catch</span> (Exception ex) {
    <span class="hljs-comment">// if command execution failed it would mean container startup fail and so exception would</span>
    <span class="hljs-comment">// be thrown. But also, testcontainers would log error and entire output </span>
}
</code></pre>
<p>In this case, the container would execute command immediately after start and would be shut down after command execution.  </p>
<p>Note that <code>.start()</code> would not wait when command execution finished, so you'll have to manually wait for container stopping:</p>
<pre><code class="lang-java"><span class="hljs-keyword">while</span> (container.isRunning() &amp;&amp; !Thread.currentThread().isInterrupted()) {
    sleep(<span class="hljs-number">300</span>)
}
</code></pre>
<p>(in case of gradle, you can't let task finish execution until docker command would be finished)</p>
<p>The downside is that we will have to lose some time starting and stopping containers (~300ms).
Another downside is <strong>losing container state</strong>: for example, python plugin requires virtualenv module installed in order to create a virtual environment, but this also means that both commands (installation and env creation) must be executed in the same container (so, in my case, it was simply impossible to always use fresh containers).</p>
<p>The advantage of this approach is that we can receive command logs immediately (<code>withLogConsumer</code>). This is important for long running (or endless) processes, like mkdocs dev server. So I have to provide such an option for python tasks. In my experiments, it takes about 10 seconds for ryuk (shutdown container) to properly remove all started containers after emergency gradle stop (IDE stop or ctrl+d in console).</p>
<p>Testcontainers has <a target="_blank" href="https://www.testcontainers.org/features/startup_and_waits/">many options to detect container readiness</a>, but in my case the simplest timeout was enough (just wait and assume container is running): <code>withStartupTimeout(Duration.ofSeconds(1))</code>. </p>
<p>It is also possible to know the exact exit code for executed command, but in a complex way:</p>
<pre><code class="lang-java"><span class="hljs-keyword">int</span> exitCode = container
  .getDockerClient()
  .inspectContainerCmd(container.getContainerId())
  .exec()
  .getState()
  .getExitCode();
</code></pre>
<h2 id="heading-in-container-command">In-container command</h2>
<p>Another approach is starting container with infinite command and execute commands inside it:</p>
<pre><code class="lang-java">GenericContainer container = <span class="hljs-keyword">new</span> GenericContainer(...)
    .withCommand(<span class="hljs-string">'tail'</span>, <span class="hljs-string">'-f'</span>, <span class="hljs-string">'/dev/null'</span>)
    .withStartupTimeout(Duration.ofSeconds(<span class="hljs-number">1</span>))  

container.start()
<span class="hljs-keyword">try</span> {
    Container.ExecResult res = container.execInContainer(StandardCharsets.UTF_8, 
                         <span class="hljs-keyword">new</span> String[]{<span class="hljs-string">"python"</span>, <span class="hljs-string">"--version"</span>})
} <span class="hljs-keyword">finally</span> {
  container.stop()
}
</code></pre>
<p>The downside is that we can obtain command output only after its execution:</p>
<pre><code class="lang-java">res.getStdout().toString();
res.getStderr().toString();
</code></pre>
<p>In python plugin, pip installation tasks are usually long enough to feel discomfort of logs absence,
but it is the only way.</p>
<p>In my case, in-container execution was a preferred way for running commands as I have many short-lived python commands and savings on not restarting the container for each command were significant.</p>
<p>It is important to note that you can configure container (working dir, environment variables and port bindings) <strong>only during container startup</strong>:</p>
<pre><code class="lang-java"><span class="hljs-keyword">new</span> GenericContainer(...)
  .withWorkingDirectory(workDir)
  .addEnv(<span class="hljs-string">"SOME_ENV"</span>, <span class="hljs-string">"value"</span>)
  .withExposedPorts(<span class="hljs-number">8080</span>)               <span class="hljs-comment">// more on this below</span>
</code></pre>
<p>For example, when you need a different working directory, you'll have to restart the container.
In case of a python plugin, each python task could declare its own directory, environment and ports and so, to properly support this I have to validate current container parameters with the upcoming command requirements and restart container automatically when necessary. </p>
<p>That caused unavoidable restart for mkdocs plugin: python environment tasks use root project dir as working directory, but all mkdocs tasks use mkdocs sources directory as home and so in any case container have to be restarted (but single restart is not a problem). </p>
<h1 id="heading-custom-container">Custom container</h1>
<p>Most likely, you'll have to create your custom container class because:</p>
<ul>
<li>It is the only way to do static port bindings</li>
<li>It is the only way to hide container errors in logs</li>
</ul>
<pre><code class="lang-java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomContainer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">GenericContainer</span>&lt;<span class="hljs-title">CustomContainer</span>&gt; </span>{

    PythonContainer(String image) {
        <span class="hljs-keyword">super</span>(DockerImageName.parse(image))
    }
}
</code></pre>
<h2 id="heading-startup-errors-logging">Startup errors logging</h2>
<p>If container startup fails, testcontainers will throw an exception and log it together with recorded output. This is very handy in the test environment, but not desired in the plugin because it would duplicate the plugin's own logs (besides, in case of python plugin, python command failure might not be a problem at all).</p>
<p>The only way to hide these logs is to override logger creation in custom container class:</p>
<pre><code class="lang-java">    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">protected</span> Logger <span class="hljs-title">logger</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-comment">// avoid direct logging of errors (prevent duplicates in log)</span>
        <span class="hljs-keyword">return</span> NOPLoggerFactory.newInstance().getLogger(CustomContainer.name)
    }
</code></pre>
<p>Note: without the <code>--stacktrace</code> option, gradle would show only the top-most exception message in the console (as a reason for failure). Testcontainers exceptions are hierarchical: for example, <code>Container error -&gt; Container failed to start -&gt; Port 8080 already in use</code>. In order to bring more clarity for the end used, it is better to collect all errors in the hierarchy and put it the top-most exception.</p>
<pre><code class="lang-java"><span class="hljs-keyword">try</span> {
  container.start()
} <span class="hljs-keyword">catch</span> (Exception ex) {
    String msg = <span class="hljs-comment">// collect all messages through hierarchy</span>
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> GradleException(<span class="hljs-string">"Container startup failed: \n"</span> + msg);
}
</code></pre>
<h2 id="heading-port-bindings">Port bindings</h2>
<p>By default, testcontainers use random port numbers: when you expose port with <code>.withExposedPorts(8080)</code> it would be actually bound to a <strong>random</strong> and free host port.
You can obtain the actual assigned port using <code>container.getMappedPort(8080)</code>.</p>
<p>This is perfect for tests, but in case of gradle plugin, fixed port bindings are required.</p>
<p>Testcontainers provide <code>FixedHostPortGenericContainer</code> to allow using fixed mappings, but it is deprecated (because it's not recommended). If we look into <a target="_blank" href="https://github.com/testcontainers/testcontainers-java/blob/08f75a5217f601439ef426b059908164a02deba4/core/src/main/java/org/testcontainers/containers/FixedHostPortGenericContainer.java">implementation</a> we'll see that <code>GenericContainer</code> already contains protected method <code>withFixedExposedPort</code>, and so we can simply add a public method in our custom container class and use it:</p>
<pre><code class="lang-java">  <span class="hljs-function">CustomContainer <span class="hljs-title">withFixedExposedPort</span><span class="hljs-params">(<span class="hljs-keyword">int</span> hostPort, <span class="hljs-keyword">int</span> containerPort)</span></span>{
        <span class="hljs-keyword">super</span>.addFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP)
        <span class="hljs-keyword">return</span> self()
    }
</code></pre>
<p>And now ports binding would look like:</p>
<pre><code class="lang-java"><span class="hljs-keyword">new</span> CustomContainer(...)
  .withFixedExposedPort(<span class="hljs-number">8080</span>, <span class="hljs-number">8080</span>)  <span class="hljs-comment">// container 8080 to host 8080</span>
  .withFixedExposedPort(<span class="hljs-number">9000</span>, <span class="hljs-number">9090</span>)  <span class="hljs-comment">// container 9090 to host 9000</span>
</code></pre>
<h1 id="heading-putting-it-all-together">Putting it all together</h1>
<p>For python plugin requirements appear to be:</p>
<ul>
<li>As plugin execute a lot of python commands, one container should be used for all of them</li>
<li>There must be an ability to run command in "exclusive" container for long-running tasks (for live logs)</li>
<li>Fixed ports bindings required (much simpler to use in plugin then random ports)</li>
<li>Containers are stateless - they must be destroyed after execution (some "caches" might be stored in project directory (like virtualenv))</li>
<li>Project directory mapped into container<ul>
<li>Root user rights fix required</li>
<li>Automatic path conversions required (in case of plugin it might be used directly with installed python and with docker). This also assumes OS specific corrections (linux containers could be started from windows).</li>
</ul>
</li>
</ul>
<p>Simple wrapper could easily abstract all docker-related staff:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ContainerManager</span> </span>{
    <span class="hljs-keyword">private</span> String image;
    <span class="hljs-keyword">private</span> CustomContainer container;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ContainerManager</span><span class="hljs-params">(String image)</span> </span>{
      <span class="hljs-keyword">this</span>.image = image;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">start</span><span class="hljs-params">()</span> </span>{
      <span class="hljs-keyword">if</span> (container == <span class="hljs-keyword">null</span>) {
          (container = createContainer())
              <span class="hljs-comment">// container with infinite command</span>
              .withCommand(<span class="hljs-string">'tail'</span>, <span class="hljs-string">'-f'</span>, <span class="hljs-string">'/dev/null'</span>)
              .start();
      }
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">stop</span><span class="hljs-params">()</span> </span>{
      <span class="hljs-keyword">if</span> (container != <span class="hljs-keyword">null</span>) {
          container.stop();
          container = <span class="hljs-keyword">null</span>;
      }
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">exec</span><span class="hljs-params">(OutputStream out, String[] command)</span> </span>{
         <span class="hljs-comment">// note: command might need paths re-mapping (to convert native paths to docker paths)</span>
         Container.ExecResult res = container.execInContainer(StandardCharsets.UTF_8, command);
         <span class="hljs-comment">// delayed output</span>
         <span class="hljs-keyword">if</span> (res.stdout != <span class="hljs-keyword">null</span> ) out.write(res.stdout.getBytes(StandardCharsets.UTF_8))
         <span class="hljs-keyword">if</span> (res.stderr != <span class="hljs-keyword">null</span> ) out.write(res.stdout.getBytes(StandardCharsets.UTF_8))
         <span class="hljs-comment">// check correctness</span>
         <span class="hljs-keyword">if</span> (res.exitCode != <span class="hljs-number">0</span>) {
              <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Execution failed"</span>)
         }
    } 

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">execExclusive</span><span class="hljs-params">(OutputStream out, String[] command)</span> </span>{
         <span class="hljs-comment">// simply starting new container exclusively for one command</span>
         CustomContainer cont = createContainer()
         <span class="hljs-keyword">try</span> {
              cont.withCommand(command)
                  <span class="hljs-comment">// immediate logs streaming</span>
                  .withLogConsumer { OutputFrame frame -&gt; out.write(frame.bytes == <span class="hljs-keyword">null</span> ? <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">0</span>]:  frame.bytes) }
                  .start()

              <span class="hljs-comment">// wait for execution to finish (interruption check detect gradle task interruption)</span>
              <span class="hljs-keyword">while</span> (cont.isRunning() &amp;&amp; !Thread.currentThread().isInterrupted()) {
                    sleep(<span class="hljs-number">300</span>)
               }
         } <span class="hljs-keyword">catch</span> (Exception ex) {
               <span class="hljs-comment">// hide error</span>
         } <span class="hljs-keyword">finally</span> {
               cont.stop(); <span class="hljs-comment">// for emergency ending (e.g. after ctrl + d)</span>
         }

        <span class="hljs-comment">// in case if you need exact exit code, otherwise exception above will always mean error</span>
        <span class="hljs-keyword">int</span> exitCode = container.getDockerClient().inspectContainerCmd(container.getContainerId())  
            .exec().getState().getExitCode(); 

        <span class="hljs-keyword">if</span> (exitCode != <span class="hljs-number">0</span>) {
              <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Execution failed"</span>)
         }
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> CustomContainer <span class="hljs-title">createContainer</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> CustomContainer(image)
            .withStartupTimeout(Duration.ofSeconds(<span class="hljs-number">1</span>))
            <span class="hljs-comment">// just an examples (variables declarations omitted)</span>
            .withFileSystemBind(projectRootPath, projectDockerPath, BindMode.READ_WRITE)
            .withWorkingDirectory(projectDockerPath)
            .addEnv(<span class="hljs-string">"SOME_ENV"</span>, <span class="hljs-string">"12"</span>)
            .withFixedExposedPort(<span class="hljs-number">8080</span>, <span class="hljs-number">8080</span>)
    }
}
</code></pre>
<p>Note that this is not a working solution - just highlighting of the main moments (in real case, you'll have to take synchronization into account and properly implement container configuration). </p>
<p>My intention was only to show the main paths and pitfalls of how testcontainers could be used to relatively easily implement docker executions.</p>
<p>If you would need to do a gradle plugin for python module, you can use <a target="_blank" href="https://github.com/xvik/gradle-use-python-plugin">python plugin</a> directly and get docker support out of the box (see <a target="_blank" href="https://github.com/xvik/gradle-mkdocs-plugin">mkdocs plugin</a> implementation as reference).</p>
<p>Or see <a target="_blank" href="https://github.com/xvik/gradle-use-python-plugin/blob/master/src/main/groovy/ru/vyarus/gradle/plugin/python/cmd/docker/ContainerManager.groovy">python plugin implementation</a> for more implementation details.</p>
]]></content:encoded></item><item><title><![CDATA[Using JUnit 5 extensions in Spock 2 tests]]></title><description><![CDATA[Spock 2 does not support JUnit 5 extensions out of the box:

We looked at adding a jupiter extension support module, but quickly dismissed it and decided to focus on finishing Spock 2.0, since we would have to emulate a large chunk of Jupiters intern...]]></description><link>https://blog.vyarus.ru/using-junit-5-extensions-in-spock-2-tests</link><guid isPermaLink="true">https://blog.vyarus.ru/using-junit-5-extensions-in-spock-2-tests</guid><category><![CDATA[Java]]></category><category><![CDATA[junit]]></category><category><![CDATA[unit testing]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Sat, 05 Mar 2022 21:23:27 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://spockframework.org/">Spock 2</a> does not support <a target="_blank" href="https://junit.org/junit5/">JUnit 5</a> extensions <a target="_blank" href="https://github.com/spockframework/spock/issues/617#issuecomment-698315195">out of the box</a>:</p>
<blockquote>
<p>We looked at adding a jupiter extension support module, but quickly dismissed it and decided to focus on finishing Spock 2.0, since we would have to emulate a large chunk of Jupiters internals to make it work.</p>
</blockquote>
<p>It is a problem for two (obvious) reasons:</p>
<ol>
<li>JUnit is much more popular and there is a great chance to find existing JUnit 5 extension, but almost no chance to find a Spock one.</li>
<li>For developer, maintaining two similar extensions for JUnit and Spock is a problem too: in JUnit 4 times Spock extensions model allows building much nicer extensions, but JUnit 5 abilities are almost equivalent.</li>
</ol>
<p>I faced the later situation with <a target="_blank" href="https://github.com/xvik/dropwizard-guicey">dropwizard-guicey</a>: currently it provides separate implementations for JUnit 4, JUnit 5 and Spock 1 and all guicey tests using Spock 1. But Spock 1 can't run on jdk16 or above which makes impossible for me to test guicey itself with jdk16 or 17.</p>
<p>Instead of implementing separate Spock 2 extensions, I decided to try implementing JUnit 5 extensions support and here it is: <a target="_blank" href="https://github.com/xvik/spock-junit5">spock-junit5</a>.</p>
<p>With it, <a target="_blank" href="https://github.com/xvik/spock-junit5#what-is-supported">alsmost any</a> JUnit 5 extension could be used in Spock test (including custom annotations, parameters injection etc):</p>
<pre><code class="lang-java"><span class="hljs-meta">@ExtendWith(JunitExt)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Test</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Specification</span> </span>{

   def <span class="hljs-string">"Check something"</span>() {
       when: <span class="hljs-string">"do something"</span>
       ...
       then: <span class="hljs-string">"some condition is correct"</span>
       ...
   }
}
</code></pre>
<p>This post is not a user guide: you can always find all usage details in the <a target="_blank" href="https://github.com/xvik/spock-junit5">project home page</a>. Instead, It will highlight the motivation and some technical details. </p>
<h2 id="heading-why-spock">Why Spock</h2>
<p>If JUnit 5 is so much better then JUnit 4 maybe it's time to get rid of Spock?
<strong>No</strong>, Spock is still way ahead of JUnit in terms of writing tests:</p>
<ol>
<li>Spock tests are more structured and self-descriptive</li>
<li>Groovy allows writing much more compact and better-redable tests</li>
<li>Spock errors reporting is wonderful</li>
<li>Parametrized tests in Spock are compact and easy to read</li>
</ol>
<p>Small example:</p>
<pre><code class="lang-java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TypesCompatibilityTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Specification</span> </span>{

    def <span class="hljs-string">"Check types compatibility"</span>() {

        expect: 
        TypeUtils.isCompatible(type1, type2) == res

        where:
        type1                          | type2                       | res
        String                         | Integer                     | <span class="hljs-keyword">true</span>
        Object                         | Integer                     | <span class="hljs-keyword">true</span>
        String                         | Object                      | <span class="hljs-keyword">true</span> 
    }
}
</code></pre>
<p>And in case of error:</p>
<pre><code>Condition not satisfied:

TypeUtils.isCompatible(type1, type2) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> res
<span class="hljs-operator">|</span>         <span class="hljs-operator">|</span>            <span class="hljs-operator">|</span>      <span class="hljs-operator">|</span>      <span class="hljs-operator">|</span>  <span class="hljs-operator">|</span>
<span class="hljs-operator">|</span>         <span class="hljs-literal">false</span>        <span class="hljs-operator">|</span>      <span class="hljs-operator">|</span>      <span class="hljs-operator">|</span>  <span class="hljs-literal">true</span>
<span class="hljs-operator">|</span>                      <span class="hljs-operator">|</span>      <span class="hljs-operator">|</span>      <span class="hljs-literal">false</span>
<span class="hljs-operator">|</span>                      <span class="hljs-operator">|</span>      class java.lang.Integer
<span class="hljs-operator">|</span>                      class java.lang.String
class ru.vyarus.java.generics.resolver.util.TypeUtils
</code></pre><p>Nice, isn't it? And no way to get anything near like this in JUnit.</p>
<p>Have to admit that sometimes (rare!) it cause additional problems: groovy compiler is not a java compiler and (in very! rare cases) there might be unexpected compilation problems with groovy classes (not tests, but additional classes, required for test). But you can always convert such classes to java (in IDEA there is an action for it) and everything will work. Besides, groovy 3 and 4 get much closer to java native compilation behaviour.   </p>
<p>Using Spock (heavily) for years I have never faced cases I couldn't workaround.</p>
<h2 id="heading-extension-models">Extension models</h2>
<p>Spock and JUnit 5 extension models are both driven by annotations: you must put an annotation somewhere in test to activate extension (global extensions also possible, but almost never used).</p>
<p>But extension implementation approaches are different.</p>
<h3 id="heading-spock-extension">Spock extension</h3>
<p>In Spock your extension must implement <code>IAnnotationDrivenExtension</code> where you must override <a target="_blank" href="https://spockframework.org/spock/docs/2.1/extensions.html#_annotation_driven_local_extensions">one of its methods</a>. It is already a bit confusing because you need to implement methods based on annotation target (different for class, field and test method annotations).</p>
<p>Inside this method you can apply an <a target="_blank" href="https://spockframework.org/spock/docs/2.1/extensions.html#_interceptors">interceptor</a>. This is very flexible, as you can hook in almost any lifecycle stage, but requires some knowledge (what to intercept and how to register interceptor properly).</p>
<p>For example, suppose extension annotation would be used on test class and extension would intercept test method execution:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Retention(RetentionPolicy.RUNTIME)</span>
<span class="hljs-meta">@Target(ElementType.TYPE)</span>
<span class="hljs-meta">@ExtensionAnnotation(MyExtImpl)</span>
<span class="hljs-meta">@interface</span> MyExt { }

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyExtImpl</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">IAnnotationDrivenExtension</span>&lt;<span class="hljs-title">MyExt</span>&gt; </span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">visitSpecAnnotation</span><span class="hljs-params">(MyExt annotation, SpecInfo spec)</span> </span>{
        spec.allFeatures*.featureMethod*.addInterceptor { invocation -&gt;

            <span class="hljs-comment">// do something before test method</span>

            invocation.proceed()

            <span class="hljs-comment">// do something after test</span>
        }

   }
}
</code></pre>
<p>Here interceptor implicitly implements <code>IMethodInterceptor</code> (note that <code>invocation.proceed()</code> is required). </p>
<p>But often it is required to hook into multiple places and here you can use <code>AbstractMethodInterceptor</code> with pre-defined lifecycle methods. The only problem would be to properly register such intercepter in all required places (<a target="_blank" href="https://spockframework.org/spock/docs/2.1/extensions.html#_interceptors">Spock docs highlight</a> all possible places).</p>
<p>Same example, but extension hooks around test methods and setup test stage:</p>
<pre><code class="lang-java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyExtImpl</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">IAnnotationDrivenExtension</span>&lt;<span class="hljs-title">MyExt</span>&gt; </span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">visitSpecAnnotation</span><span class="hljs-params">(MyExt annotation, SpecInfo spec)</span> </span>{
        MyInterceptor interceptor = <span class="hljs-keyword">new</span> MyIntrerceptor()

        spec.addSetupInterceptor interceptor
        spec.allFeatures*.featureMethod*.addInterceptor interceptor
   }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyInterceptor</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractMethodInterceptor</span> </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">interceptSetupMethod</span><span class="hljs-params">(<span class="hljs-keyword">final</span> IMethodInvocation invocation)</span> </span>{
          <span class="hljs-comment">// do something</span>
          invocation.proceed()
     }

     <span class="hljs-meta">@Override</span>
     <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">interceptFeatureMethod</span><span class="hljs-params">(<span class="hljs-keyword">final</span> IMethodInvocation invocation)</span> </span>{
          <span class="hljs-comment">// do something</span>
          invocation.proceed()
     }
}
</code></pre>
<p>As you can see, very flexible, but not very easy to understand model. And, by the way, it might not be obvious, but Spock extensions does not require groovy and could be written in java (Spock itself is written in java).</p>
<p>(More details in <a target="_blank" href="https://spockframework.org/spock/docs/2.1/extensions.html#_writing_custom_extensions">Spock extensions guide</a>)</p>
<h3 id="heading-junit-5-extension">Junit 5 extension</h3>
<p>In JUnit 5 there is a set of extension interfaces:</p>
<ul>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-conditions">ExecutionCondition</a></li>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-lifecycle-callbacks">BeforeAllCallback</a></li>
<li>AfterAllCallback</li>
<li>BeforeEachCallback</li>
<li>AfterEachCallback</li>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-lifecycle-callbacks-before-after-execution">BeforeTestExecutionCallback</a></li>
<li>AfterTestExecutionCallback</li>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution">ParameterResolver</a></li>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-test-instance-post-processing">TestInstancePostProcessor</a></li>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-test-instance-pre-destroy-callback">TestInstancePreDestroyCallback</a></li>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-exception-handling">TestExecutionExceptionHandler</a></li>
<li>(and a few more)</li>
</ul>
<p>All of them extends base <code>Extension</code> class.</p>
<p>Actual extension implement required interfaces. And extension from Spock example would look like:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyExtension</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BeforeEachCallback</span>, 
                                    <span class="hljs-title">BeforeTestExecutionCallback</span>,
                                    <span class="hljs-title">AfterTestExecutionCallback</span> </span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">beforeEach</span><span class="hljs-params">(ExtensionContext context)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-comment">// do something</span>
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">beforeTestExecution</span><span class="hljs-params">(ExtensionContext context)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-comment">// do something</span>
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">afterTestExecution</span><span class="hljs-params">(ExtensionContext context)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-comment">// do something</span>
    }
}
</code></pre>
<p>Extension activation:</p>
<pre><code class="lang-java"><span class="hljs-meta">@ExtenWith(MyExtensions.class)</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTest</span> </span>{ ... }
</code></pre>
<p>Or you can make a custom annotation:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Target(ElementType.TYPE)</span>
<span class="hljs-meta">@Retention(RetentionPolicy.RUNTIME)</span>
<span class="hljs-meta">@ExtendWith(MyExtensions.class)</span>
<span class="hljs-keyword">public</span> <span class="hljs-meta">@interface</span> MyExt {}
</code></pre>
<p>(More details in <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions">JUnit extension guide</a>)</p>
<p>As you can see, JUnit extensions are very easy to write and understand. They are a bit less powerful then Spock extensions, but, to be honest, I doubt it would be a problem in the majority of cases. </p>
<p>So JUnit extensions are ideal for writing test integrations.</p>
<h3 id="heading-lifecycles-compatibility">Lifecycles compatibility</h3>
<p>JUnit and Spock share pretty much the same lifecycle (I did a <a target="_blank" href="https://github.com/xvik/spock-junit5#lifecycle">comparison table</a>). </p>
<p>Differences:</p>
<ol>
<li>Spock owns test instance creation  and so it is impossible to simulate JUnit <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-test-instance-factories">test instance factory</a></li>
<li>Spock does not allow constructors (and so parameter injection in constructor is impossible)</li>
<li>Spock has a <a target="_blank" href="https://spockframework.org/spock/docs/2.1/spock_primer.html#_fields">shared fields concept</a> (more on it below)</li>
</ol>
<p>In Spock, "shared fields" is a core feature, but it is implemented with an additional test instance: shared instance contains all shared fields and a new test instance created for each test method. When you accessing shared fields from test method it is "magically" routed to different (shared) instance.</p>
<p>There are two initialization hooks in Spock (each with different test instance):</p>
<ul>
<li><code>specInfo.addSharedInitializerInterceptor</code></li>
<li><code>specInfo.addInitializerInterceptor</code></li>
</ul>
<p>In JUnit, shared fields could be simulated with <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle">@TestInstance(LifeCycle.PER_CLASS)</a>. With it we will have a single test instance for all tests (by default, there are different instances for each test method).</p>
<p>JUnit extensions always receive a context object (as a parameter). It could be class-level context or method-level context containing test instance reference (there are other types, but in general these are the most important). If I would try to support shared fields, I'll have to call some JUnit extensions (like BeforeEach) two times, which may break extensions internal state. So it is not possible to support Spock shared fields for JUnit extensions (only one instance could be used in context).</p>
<p>Shared fields would be "invisible" for JUnit extensions: even if JUnit extension tried to initialize Spock shared field with reflection (it's just a field) - it would do it on "test" instance, but Spock magic would make this value invisible as every access to shared field is routed to a different instance.</p>
<p>Anyway, it should not be a problem because shared fields are not used often. </p>
<h2 id="heading-copy-paste">Copy paste</h2>
<p>Here I should quote again Spock maintainers:</p>
<blockquote>
<p>We looked at adding a jupiter extension support module, but quickly dismissed it and decided to focus on finishing Spock 2.0, since we would have to emulate a large chunk of Jupiters internals to make it work.</p>
</blockquote>
<p>That is so true! I have to copy-paste a lot from junit-jupiter-engine (to grant the same behaviour). </p>
<p>Jupiter is a complete test engine and so it contains all the code required for test execution (fixture method calls, test method calls, exception handling etc.). As a result, it has some additional abstractions, which are not required in context of spock.</p>
<p>The root jupiter concept is descriptor (internal). There are multiple context levels: <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java">root</a> with global extensions and configurations, <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java">test level</a>, <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java">method level</a> etc. Each descriptor contains  (among other staff) extension resolution and execution) logic. For example, class level context - class level extensions <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java#L148">resolution</a> and <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java#L389">before/after all calls</a>. Method level context - method level extension <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java#L119">resolution</a> and <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java#L164">before/after each calls</a> (and others ofc.).</p>
<p>It was the most complex part: in context of spock, tests execution code is obviously not required and so only extension-related logic must be copy-pasted (and highly adapted) in order to preserve the same extensions behaviour. Extension registration logic was collected (from various places) into <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/engine/ExtensionUtils.java">ExtensionUtils</a> and all execution logic (extensions processing) into <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/interceptor/JunitApiExecutor.java">JupiterApiExecutor</a>.</p>
<p>It was important to preserve the same extensions order and even exceptions behaviour (that's why copy-pasting was important).</p>
<p>As was already mentioned, JUnit extensions require context objects. JUnit use ~5 context <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java">implementations</a>, but in terms of integration only 2 are important: <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java">class context</a> and <a target="_blank" href="https://github.com/junit-team/junit5/blob/main/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java">method context</a>. It was also impossible to simply copy-paste existing jupiter implementations as they are too tied to internal contexts. </p>
<p>Contexts were <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/engine/context/AbstractContext.java">re-implemented</a> (they are quite simple). In context of spock many abilities are not used at all and so many context methods simply return empty objects.</p>
<p>Thankfully, there are <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-supported-utilities">shared utilities in JUnit</a> which might be used without copy-pasting.
For example, annotations search logic could be re-used through <code>org.junit.platform.commons.util.AnnotationUtils</code> class (not <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-supported-utilities-annotations">the official API</a>, but also available).</p>
<p>And these missing parts were copied almost as-is:</p>
<ul>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#extensions-keeping-state">Value storage</a> implementation (<a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/engine/store/ExtensionValuesStore.java">ExtensionValuesStore</a>) - exact copy</li>
<li><a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-conditional-execution">Condition</a> evaluation (<a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/engine/execution/ConditionEvaluator.java">ConditionEvaluator</a>) - exact copy</li>
<li>ParameterContext implementation for parameter resolver extensions (<a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/engine/context/DefaultParameterContext.java">DefaultParameterContext</a>) - exact copy</li>
<li>Extensions registry (<a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/engine/ExtensionRegistry.java">ExtensionRegistry</a>) - with simplifications because in JUnit there are extension tiers which are not needed in context of integration</li>
</ul>
<h2 id="heading-integration">Integration</h2>
<p>JUnit extensions integration was <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/JunitExtensionSupport.java">implemented</a> as <a target="_blank" href="https://spockframework.org/spock/docs/2.1/extensions.html#_global_extensions">global Spock extension</a>. So you don't need to use any additional annotations to activate it: JUnit extensions registration will work <a target="_blank" href="https://github.com/xvik/spock-junit5#usage">exactly the same</a> way as in JUnit.</p>
<p>Overall algorithm is pretty simple:</p>
<ol>
<li>Try to <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/JunitExtensionSupport.java#L77">find class-level extensions</a> for each test. Here class-level JUnit context is created (without test instance)</li>
<li>Register <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/JunitExtensionSupport.java#L105">required interceptors</a> to be able to call JUnit extensions in correct Spock lifecycle stages (see <a target="_blank" href="https://github.com/xvik/spock-junit5#lifecycle">this table</a> for merged lifecycle) </li>
<li>On test method initialization (when test instance created for test method) <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/interceptor/ExtensionLifecycleMerger.java#L114">create method-level context</a> (containing test instance reference)</li>
</ol>
<p>I have to <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/engine/ExtensionUtils.java#L70">hardcode</a> two lists of extension types: <a target="_blank" href="https://github.com/xvik/spock-junit5#what-is-supported">supported and not supported</a>.
If not supported extension appear I could warn user about it in log (but not crash because it might be not important and everything would work without it).</p>
<p>If no known extension types recognized in registered extension - execution would would fail because nothing could be done in this case: all extension types have to be supported manually (at exact point extension method must be called manually for all registered extensions) and so unknown extensions couldn't do anything.</p>
<p>Probably not very obvious is <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/JunitExtensionSupport.java#L91">conditional JUnit extensions support</a>: for example, JUnit <code>@Disabled</code> could skip Spock test.</p>
<p>Before that project I was sure that Spock does not support parameter injections for test or fixture methods (never read guide that far), but <a target="_blank" href="https://spockframework.org/spock/docs/2.1/extensions.html#_injecting_method_parameters">it can</a>! In fact, it keeps a marked list of parameters and all parameters Spock is not aware of are marked as <code>MethodInfo.MISSING_ARGUMENT</code>. So all I need is to <a target="_blank" href="https://github.com/xvik/spock-junit5/blob/master/src/main/java/ru/vyarus/spock/jupiter/interceptor/ExtensionLifecycleMerger.java#L200">call JUnit extensions for unknown parameters</a>.</p>
<p>Important moment here is to not throw error when parameter not matched (as JUnit did) because there might be other Spock extensions responsible for injection (which could execute after global extension).</p>
<h2 id="heading-maturity">Maturity</h2>
<p>So the project is almost completely built on copy-pasted code from jupiter engine. Ok, it will work, for now. But what about the future, when JUnit evolve (and it will)?</p>
<p>First of all, there are a lot of comments refencing original code source. So it would be possible to track and apply changes.</p>
<p>Secondly, there are two sets of tests: <a target="_blank" href="https://github.com/xvik/spock-junit5/tree/master/src/test/java/playground">first set</a> validates JUnit extensions behavior (with jupiter engine) and the <a target="_blank" href="https://github.com/xvik/spock-junit5/tree/master/src/test/groovy/ru/vyarus/spock/jupiter">second one</a> validates that Spock behaves the same. So if some JUnit behaviour will change - I will know it from the first group of tests and correct Spock behaviour accordingly.</p>
<p>Previously, I wrote separate articles about how these tests were implemented using jupiter TestKit:</p>
<ul>
<li><a target="_blank" href="https://blog.vyarus.ru/testing-junit-5-extensions">Testing JUnit 5 extensions</a></li>
<li><a target="_blank" href="https://blog.vyarus.ru/testing-spock-2-extensions">Testing Spock 2 extensions</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Testing Spock 2 extensions]]></title><description><![CDATA[This is the follow-up of Testing JUnit 5 extensions article (assume you read it first). This article shows how to test Spock 2 extensions the same way.
Like JUnit's Jupiter, Spock 2 is implemented as JUnit platform engine and so TestKit might be used...]]></description><link>https://blog.vyarus.ru/testing-spock-2-extensions</link><guid isPermaLink="true">https://blog.vyarus.ru/testing-spock-2-extensions</guid><category><![CDATA[Java]]></category><category><![CDATA[unit testing]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Mon, 10 Jan 2022 22:14:11 GMT</pubDate><content:encoded><![CDATA[<p>This is the follow-up of <a target="_blank" href="https://blog.vyarus.ru/testing-junit-5-extensions">Testing JUnit 5 extensions</a> article (assume you read it first). This article shows how to test Spock 2 extensions the same way.</p>
<p>Like JUnit's Jupiter, <a target="_blank" href="https://spockframework.org/">Spock 2</a> is implemented as JUnit platform engine and so <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#testkit-engine">TestKit</a> might be used for running Spock tests too. But there are a few differences.</p>
<p>First of all, Spock already provides a shortcut for TestKit tests: 
<code>spock.util.EmbeddedSpecRunner</code>.</p>
<p>As with JUnit, suppose we want to check extension test method fail handling (and extension should stop this exception type):</p>
<pre><code class="lang-java"><span class="hljs-meta">@MySpockExtension</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Specification</span> </span>{

    <span class="hljs-keyword">void</span> <span class="hljs-string">"test method fail"</span>() {

        when:
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Ups"</span>)

        then:
        <span class="hljs-keyword">true</span>
    }
}
</code></pre>
<pre><code class="lang-java">def specRunner = <span class="hljs-keyword">new</span> EmbeddedSpecRunner()
specRunner.throwFailure = <span class="hljs-keyword">false</span>
specRunner.runClass(FailingTestCase)
          .testEvents()
          .debug() <span class="hljs-comment">// optional </span>
          .assertStatistics(stats -&gt; stats.failed(<span class="hljs-number">1</span>));
</code></pre>
<p>By default, <code>EmbeddedSpecRunner</code> re-throws exceptions which is not desirable (have to disable it for the same behavior as in pure JUnit kit tests).</p>
<p>Also, runner supports direct test compilation so the test itself could be described as a string instead of a separate class:</p>
<pre><code class="lang-java">def specRunner = <span class="hljs-keyword">new</span> EmbeddedSpecRunner()
specRunner.throwFailure = <span class="hljs-keyword">false</span>
specRunner.run <span class="hljs-string">''</span><span class="hljs-string">'
package com.foo

import spock.lang.Specification
import spock.util.EmbeddedSpecRunner

@MySpockExtension
class extends Specification {

    void "test method fail"() {

        when:
        throw new IllegalStateException("Ups")

        then:
        true
    }
}
'</span><span class="hljs-string">''</span>
          .testEvents()
          .debug() <span class="hljs-comment">// optional </span>
          .assertStatistics(stats -&gt; stats.failed(<span class="hljs-number">1</span>));
</code></pre>
<p>This may look weird, but, probably, could be useful in some cases. At least, this solves the "failing tests problem" because the test in a string would never be recognized and executed directly. Still, I think separate test classes usage is handier.</p>
<p>You can find more <code>EmbeddedSpecRunner</code> usage examples in <a target="_blank" href="https://github.com/spockframework/spock/search?q=EmbeddedSpecRunner">spock sources</a>.</p>
<h2 id="heading-failing-tests-problem">Failing tests problem</h2>
<p>As with <a target="_blank" href="https://blog.vyarus.ru/testing-junit-5-extensions#heading-failing-tests-problem">JUnit tests</a>, failing test cases (used for TestKit tests) would be found and executed directly and fail the build. 
Spock does not support JUnit constraints and so <code>@Disabled</code> can't be used to <a target="_blank" href="https://blog.vyarus.ru/testing-junit-5-extensions#heading-failing-tests-problem">skip "test scenarios" from separate execution</a>. Spock <a target="_blank" href="https://spockframework.org/spock/docs/2.0/extensions.html#_ignore">@Ignore</a> extension does not provide a way to disable for TestKit execution and that's why I have to use the following hack:</p>
<p><code>static Boolean ACTIVE = false</code> with <a target="_blank" href="https://spockframework.org/spock/docs/2.0/extensions.html#_requires">@Requires</a> spock extension:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Requires({ AbstractTest.ACTIVE })</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestCase</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Specification</span> </span>{ ...
</code></pre>
<p>Where <code>AbstractTest.ACTIVE</code> is set to true before each TestKit test (full source below) and reverted to false after.</p>
<p>You can evolve this approach for your case if required.</p>
<h2 id="heading-testing-parallel-execution">Testing parallel execution</h2>
<p>As in JUnit, concurrent execution of test methods might be configured with <code>@Execution(ExecutionMode.CONCURRENT)</code> class annotation. IMPORTANT this is Spock annotation, not JUnit one (classes are the same just in different packages).</p>
<p>Executing several tests in parallel:</p>
<pre><code class="lang-java">def specRunner = <span class="hljs-keyword">new</span> EmbeddedSpecRunner()
specRunner.throwFailure = <span class="hljs-keyword">false</span>
specRunner.configurationScript {
         runner {
             parallel {
                 enabled <span class="hljs-keyword">true</span>
                 defaultExecutionMode ExecutionMode.CONCURRENT
                 defaultSpecificationExecutionMode ExecutionMode.CONCURRENT
             }
         }
}
specRunner.runClasses([Test1, Test2, Test3, Test4])
      .testEvents()
      .debug() <span class="hljs-comment">// optional</span>
      .assertStatistics(stats -&gt; stats.succeeded(<span class="hljs-number">4</span>));
</code></pre>
<p>WARNING: there should not be variables called the same as sections inside <code>configurationScript</code> (for example, runner, parallel, etc.) otherwise you'll see very non-intuitive errors.</p>
<p>In the example above both specs and spec methods would run in parallel, but you can configure differently. Also, note that for data-driven tests (with <code>where</code> section) each iteration is a different execution and so iterations could run in parallel too.</p>
<h2 id="heading-simple-testing-technique">Simple testing technique</h2>
<p>The <a target="_blank" href="https://blog.vyarus.ru/testing-junit-5-extensions#heading-simple-testing-technique">base test from JUnit article</a> would look like this for Spock:  </p>
<pre><code class="lang-java"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AbstractTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Specification</span> </span>{

    <span class="hljs-comment">// used for activating tests only for TestKit run</span>
    <span class="hljs-keyword">static</span> Boolean ACTIVE = <span class="hljs-function"><span class="hljs-keyword">false</span>

    List&lt;String&gt; <span class="hljs-title">runTest</span><span class="hljs-params">(Class test)</span> </span>{
        ActionHolder.cleanup()
        ACTIVE = <span class="hljs-keyword">true</span>
        <span class="hljs-keyword">try</span> {
            def specRunner = <span class="hljs-keyword">new</span> EmbeddedSpecRunner()
            <span class="hljs-comment">// do not rethrow exception - all errors will remain in holder</span>
            specRunner.throwFailure = <span class="hljs-keyword">false</span>
            specRunner.runClass(test)
                    .allEvents().failed().stream()
                    <span class="hljs-comment">// exceptions appended to events log</span>
                    .forEach(event -&gt; {
                        Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get();
                        ActionHolder.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage())
                    })
            <span class="hljs-keyword">return</span> ActionHolder.getState()
        } <span class="hljs-keyword">finally</span> {
            ACTIVE = <span class="hljs-keyword">false</span>
            ActionHolder.cleanup()
        }
    }
}
</code></pre>
<p>Here, as in JUnit example, all execution errors are collected as string messages, so when anything goes wrong the list of the resulting strings would be different.</p>
<p>Complete example case for testing parameters injection (assuming extension injects required parameter):</p>
<pre><code class="lang-java"><span class="hljs-meta">@Requires({ AbstractTest.ACTIVE })</span>
<span class="hljs-meta">@MySpockExtension</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExceptionCase</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Specification</span> </span>{

    def <span class="hljs-string">"Sample test"</span>(Integer arg) {

        when:
        ActionHolder.add(<span class="hljs-string">"test.body $arg"</span>)
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">'Ups'</span>)

        then:
        <span class="hljs-keyword">true</span>
    }
}
</code></pre>
<p>And the main TestKit test:</p>
<pre><code class="lang-java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpockTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractTest</span> </span>{

    def <span class="hljs-string">"Check parameter"</span>() {

        expect: <span class="hljs-string">'test fails'</span>
        runTest(ExceptionCase) == [
                                  <span class="hljs-string">"test.body 11"</span>,
                                  <span class="hljs-string">"Error: (IllegalStateException) Ups"</span>]
    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Testing JUnit 5 extensions]]></title><description><![CDATA[Testing extensions always requires testing parallel/non-parallel execution and post-test callbacks validation. It is impossible to do with regular tests, but, thankfully, JUnit 5 provides a TestKit for running and recording test execution (so you can...]]></description><link>https://blog.vyarus.ru/testing-junit-5-extensions</link><guid isPermaLink="true">https://blog.vyarus.ru/testing-junit-5-extensions</guid><category><![CDATA[Java]]></category><category><![CDATA[junit]]></category><category><![CDATA[unit testing]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Sun, 09 Jan 2022 12:06:12 GMT</pubDate><content:encoded><![CDATA[<p>Testing extensions always requires testing parallel/non-parallel execution and post-test callbacks validation. It is impossible to do with regular tests, but, thankfully, JUnit 5 provides a <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#testkit">TestKit</a> for running and recording test execution (so you can assert entire test engine execution).</p>
<p>TestKit dependency will be required: <code>org.junit.platform:junit-platform-testkit</code></p>
<p>All required dependencies (gradle example):</p>
<pre><code><span class="hljs-attribute">testImplementation</span> 'org.junit.jupiter:junit-jupiter-api:<span class="hljs-number">5</span>.<span class="hljs-number">8</span>.<span class="hljs-number">2</span>'
<span class="hljs-attribute">testImplementation</span> 'org.junit.platform:junit-platform-testkit:<span class="hljs-number">5</span>.<span class="hljs-number">8</span>.<span class="hljs-number">2</span>'
<span class="hljs-attribute">testRuntimeOnly</span> 'org.junit.jupiter:junit-jupiter:<span class="hljs-number">5</span>.<span class="hljs-number">8</span>.<span class="hljs-number">2</span>'
</code></pre><p>Suppose we want to test situation when test throws an exception (because our extension implements <code>TestExecutionExceptionHandler</code> and we need to test its behaviour). </p>
<p>Writing test implementing required scenario:</p>
<pre><code class="lang-java"><span class="hljs-meta">@ExtendWith(MyExtension.class)</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FailingTestCase</span> </span>{

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">test</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Ups"</span>);
    }
}
</code></pre>
<p>And now the main (TestKit) test:</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyExtensionFailureBypassTest</span> </span>{

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">testExceptionBypass</span><span class="hljs-params">()</span> </span>{
       EngineTestKit
             .engine(<span class="hljs-string">"junit-jupiter"</span>)
             .selectors(selectClass(FailingTestCase.class))
             .execute()
             .testEvents()
             .debug() // optional 
             .assertStatistics(stats -&gt; stats.failed(<span class="hljs-number">1</span>));
    }
}
</code></pre>
<p>Here we test that exception was thrown (suppose extension should not handle this type of exceptions). More assertion technics could be found in <a target="_blank" href="https://junit.org/junit5/docs/current/user-guide/#testkit">junit docs</a> - I just want to show the base idea here.</p>
<p>Note that there are different scopes of events: <code>.testEvents()</code>, <code>.containerEvents()</code> and <code>.allEvents()</code>.</p>
<h2 id="heading-failing-tests-problem">Failing tests problem</h2>
<p>There is one problem: test scenario is also a valid junit test and so it would be found and executed during all tests execution. To avoid it annotating test case as disabled:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Disabled</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FailingTestCase</span> </span>{ ...
</code></pre>
<p>And in TestKit test ignoring this annotation:</p>
<pre><code class="lang-java">EngineTestKit
       .engine(<span class="hljs-string">"junit-jupiter"</span>)
       .configurationParameter(<span class="hljs-string">"junit.jupiter.conditions.deactivate"</span>, <span class="hljs-string">"org.junit.*DisabledCondition"</span>)
</code></pre>
<p>Now only TestKit tests will be executed and all "scenario" tests would be ignored.</p>
<h2 id="heading-testing-parallel-execution">Testing parallel execution</h2>
<p>Parallel execution for several tests could be configured only on engine level so all tests execution is either parallel or not (on test class level parallel methods execution could be activated with <code>@Execution(ExecutionMode.CONCURRENT)</code> annotation). With TestKit you can run just a few tests in parallel and validate behavior:</p>
<pre><code class="lang-java">EngineTestKit
      .engine(<span class="hljs-string">"junit-jupiter"</span>)
      .configurationParameter(<span class="hljs-string">"junit.jupiter.conditions.deactivate"</span>, <span class="hljs-string">"org.junit.*DisabledCondition"</span>)
       <span class="hljs-comment">// enable parallel execution</span>
      .configurationParameter(<span class="hljs-string">"junit.jupiter.execution.parallel.enabled"</span>, <span class="hljs-string">"true"</span>)
      .configurationParameter(<span class="hljs-string">"junit.jupiter.execution.parallel.mode.default"</span>, <span class="hljs-string">"concurrent"</span>)
       <span class="hljs-comment">// use 4 tests to run in parallel</span>
      .selectors(selectClass(Test1.class),
          selectClass(Test2.class),
          selectClass(Test3.class),
          selectClass(Test4.class))
      .execute()
      .testEvents()
      .debug()
      .assertStatistics(stats -&gt; stats.succeeded(<span class="hljs-number">4</span>));
</code></pre>
<h2 id="heading-simple-testing-technique">Simple testing technique</h2>
<p>OK/KO situations testing is often not enough: often you need to keep some state from the test in order to validate it in the TestKit test. The simplest way is to introduce shared thread local state:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ActionHolder</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> ThreadLocal&lt;List&lt;String&gt;&gt; STATE = <span class="hljs-keyword">new</span> ThreadLocal&lt;&gt;();

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">cleanup</span><span class="hljs-params">()</span> </span>{
        STATE.remove();
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">add</span><span class="hljs-params">(String state)</span> </span>{
        List&lt;String&gt; st = STATE.get();
        <span class="hljs-keyword">if</span> (st == <span class="hljs-keyword">null</span>) {
            st = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();
            STATE.set(st);
        }
        st.add(state);
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> List&lt;String&gt; <span class="hljs-title">getState</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> STATE.get() == <span class="hljs-keyword">null</span> ? Collections.emptyList() : <span class="hljs-keyword">new</span> ArrayList&lt;&gt;(STATE.get());
    }
}
</code></pre>
<p>During the test, we would add strings to it and after TestKit execution would verify the results. </p>
<p>Such storage requires careful management, so it's better to move all test boilerplate into base test:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AbstractTest</span> </span>{

    <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;String&gt; <span class="hljs-title">runTest</span><span class="hljs-params">(Class test)</span> </span>{
        ActionHolder.cleanup();
        <span class="hljs-keyword">try</span> {
            EngineTestKit
                  .engine(<span class="hljs-string">"junit-jupiter"</span>)
                  .configurationParameter(<span class="hljs-string">"junit.jupiter.conditions.deactivate"</span>, <span class="hljs-string">"org.junit.*DisabledCondition"</span>)
                  .selectors(selectClass(test))
                  .execute()
                  .allEvents().failed().stream()
                  <span class="hljs-comment">// exceptions appended to events log</span>
                  .forEach(event -&gt; {
                      Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get();
                      ActionHolder.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage());
                  });
            <span class="hljs-keyword">return</span> ActionHolder.getState();
        } <span class="hljs-keyword">finally</span> {
            ActionHolder.cleanup();
        }
    }
}
</code></pre>
<p>Note that all exceptions are also added to the holder after execution, so if something goes wrong you'll know about it (no false-positive tests).</p>
<p>Let's take the initial case and assume that it also implements <code>ParameterResolver</code> and so we need to check the correctness of injected parameters:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Disabled</span>
<span class="hljs-meta">@ExtendWith(MyExtension.class)</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FailingTestCase</span> </span>{

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">test</span><span class="hljs-params">(Integer arg)</span> </span>{
        ActionHolder.add(<span class="hljs-string">"test.arg: "</span> + arg)
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Ups"</span>)
    }
}
</code></pre>
<p>TestKit test:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StateTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractTest</span> </span>{
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">testParams</span><span class="hljs-params">()</span> </span>{
        Assertions.assertEquals(Arrays.asList(
                <span class="hljs-string">"test.arg: 11"</span>,
                <span class="hljs-string">"Error: (IllegalStateException) Ups"</span>

                ), runTest(FailingTestCase.class))
    }
}
</code></pre>
<p>Here we can validate that correct parameter value was injected and the exception. Note that in any case, all errors would be at the end of the list.</p>
<p>Such tests would be safe to execute in parallel because the state is managed under a single test method only. </p>
<p>As an example, I used this technique to validate (confirm) junit lifecycle sequence. </p>
<hr />
<p>Follow up: <a target="_blank" href="https://blog.vyarus.ru/testing-spock-2-extensions">How to do the same in Spock 2</a></p>
]]></content:encoded></item><item><title><![CDATA[Comfortable Ubuntu (21.10)]]></title><description><![CDATA[I had a chance to once again configure fresh ubuntu installation and, while I remember everything, describe basic moments. For me, this post would stay as a reminder, for you, it might reveal some configuration hints.

DISCLAIMER: configuration perfo...]]></description><link>https://blog.vyarus.ru/comfortable-ubuntu-2110</link><guid isPermaLink="true">https://blog.vyarus.ru/comfortable-ubuntu-2110</guid><category><![CDATA[Ubuntu]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Tue, 28 Dec 2021 08:12:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640669609937/LHT5oJi9d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>I had a chance to once again configure fresh ubuntu installation and, while I remember everything, describe basic moments. For me, this post would stay as a reminder, for you, it might reveal some configuration hints.</p>
</blockquote>
<p>DISCLAIMER: configuration performed for desktop PC with  22" monitor  (1920x1080 resolution). The most annoying thing in Ubuntu for me is font size which I always reduce.
Also, because  <a target="_blank" href="https://blog.vyarus.ru/ubuntu-2104-and-up-vnc-access-to-a-locked-session">I need VNC access</a>, I use Xorg login instead of wayland. As I write it by memory, actions sequence may not be exact (but, anyway, all required actions should be described).</p>
<p>This is how fresh Ubuntu 21.10 looks:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640669609937/LHT5oJi9d.jpeg" alt="Ubuntu-21.10-Impish-Indri-is-ready-to-download-4.jpg" /></p>
<p>*See overall toolbars positions</p>
<p>And this is how it could look:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640668707991/IBS2Hahcw.png" alt="Screenshot from 2021-12-28 12-14-20.png" /></p>
<h2 id="heading-step-0">Step 0</h2>
<p>Install <a target="_blank" href="https://en.wikipedia.org/wiki/Ubuntu-restricted-extras">ubuntu-restricted-extras</a>:</p>
<pre><code>sudo apt <span class="hljs-keyword">install</span> ubuntu-<span class="hljs-keyword">restricted</span>-extras
</code></pre><p>Enable  <a target="_blank" href="https://help.ubuntu.com/community/Repositories/Ubuntu#Enabling_Canonical_Partner_Repositories">partner repositories </a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640670254628/zevVsXZ_9.png" alt="Other Software tab_001.png" /></p>
<p>Install <code>Tweaks</code> utility from ubuntu store:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640670718947/WCb796nHW.png" alt="Screenshot from 2021-12-28 12-51-35.png" /></p>
<h2 id="heading-language-switch-shortcut">Language switch shortcut</h2>
<p>By default language switched with <code>super + space</code>. To change it use <code>Tweaks</code> tool:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643181384450/ZF0toqhzX.png" alt="Screenshot from 2022-01-26 14-16-14.png" /></p>
<p>In the "Keyboard and Mouse" section click on "Additional Layout Options" and select, for example, <code>Alt + Shift</code> there:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643181470898/L9nvx4W9A.png" alt="Screenshot from 2022-01-26 13-59-07.png" /></p>
<h3 id="heading-scroll-lock-for-alternative-language">Scroll lock for alternative language</h3>
<p>To indicate alternative language with a scroll lock, you'll need to install <code>dconf-editor</code> application:</p>
<pre><code><span class="hljs-attribute">sudo</span> apt install dconf-editor
</code></pre><p>Start it form the apps menu and go to <code>org.gnome.desktop.input-sources</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643181633669/NkRkpTbSM.png" alt="Screenshot from 2022-01-26 14-11-37.png" /></p>
<p>Add <code>'grp_led:scroll'</code> in the "Custom value".</p>
<p>Now on each language switch (with <code>Alt + Switch</code> or manually in tray) "Scroll Lock" would indicate alternative language selection.</p>
<h2 id="heading-fonts">Fonts</h2>
<p>Install Microsoft fonts (Times New Roman, Arial):</p>
<pre><code>sudo apt install ttf<span class="hljs-operator">-</span>mscorefonts<span class="hljs-operator">-</span>installer
</code></pre><p>It will be required for chrome.</p>
<p>My preferred monospace font is <a target="_blank" href="https://www.fontmirror.com/inconsolata-g">Inconsolata-g</a>. To install it download otf version, double click, and hit install:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640671163862/Tn5JQ0WrK.png" alt="Screenshot from 2021-12-28 12-58-46.png" /></p>
<p>It would be used for console and code (IDE).</p>
<p>Also, download and install <code>Inconsolata-g for Powerlines.otf</code> <a target="_blank" href="https://github.com/powerline/fonts/tree/master/Inconsolata-g">from here</a>. This font would be required for chrome (it looks sharper).</p>
<p>Then flush fonts cache:</p>
<pre><code>sudo fc<span class="hljs-operator">-</span>cache <span class="hljs-operator">-</span>f <span class="hljs-operator">-</span>v
</code></pre><p>Configure system fonts in <code>Tweaks</code> tool:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640672186911/6skUkoxb2.png" alt="ubuntu_fonts.png" /></p>
<h2 id="heading-chrome">Chrome</h2>
<p>Add chrome source:</p>
<pre><code>sudo sh -c <span class="hljs-string">'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" &gt;&gt; /etc/apt/sources.list.d/google.list'</span>
sudo apt <span class="hljs-keyword">update</span>
</code></pre><p>Install chrome:</p>
<pre><code>sudo apt install google<span class="hljs-operator">-</span>chrome<span class="hljs-operator">-</span>stable
</code></pre><p>To remove chrome icon from tray: </p>
<pre><code>Chrome settings / Advanced / <span class="hljs-keyword">System</span>
<span class="hljs-keyword">Disable</span> "Continue running background apps when Google Chrome is closed"
</code></pre><p>NOTE: <strong>don't</strong> enable "Use GTK+" in Appearance (as it's often suggested) - tabs would look only uglier with it.</p>
<p>To avoid giant tabs (and even more giant tab hints) in chrome customize fonts:</p>
<pre><code>Chrome settings <span class="hljs-operator">/</span> Advanced <span class="hljs-operator">/</span> Appearance <span class="hljs-operator">/</span> Customize fonts
</code></pre><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640672750297/xZWwXFN8l.png" alt="chrome_fonts.png" /></p>
<p>Probably, a system restart would be required for changes to take effect (do it later).</p>
<h2 id="heading-extensions">Extensions</h2>
<p>All other customizations will require <a target="_blank" href="https://itsfoss.com/gnome-shell-extensions/">extensions</a>:</p>
<pre><code>sudo apt install chrome<span class="hljs-operator">-</span>gnome<span class="hljs-operator">-</span>shell
</code></pre><p>*Could be already installed</p>
<p>Open <a target="_blank" href="https://extensions.gnome.org/">extensions site</a>. It may ask you to install also a chrome extension (do it).</p>
<p>Here you can find and install new extensions.</p>
<p>Some extensions may be configured only from the <code>Extensions</code> application (you can run it from apps menu):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640673593736/t-T0HN7rB.png" alt="extensions.png" /></p>
<h3 id="heading-windows-bar">Windows bar</h3>
<p>The most important extension is <a target="_blank" href="https://extensions.gnome.org/extension/1160/dash-to-panel/">Dash-to-Panel</a> - windows-like bottom panel.</p>
<p>By default, the panel is larger than it should be. To reduce panel size, open options and reduce "Panel thickness" setting below 48:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640673850031/2kPBW1zY8.png" alt="Screenshot from 2021-12-28 13-43-27.png" /></p>
<p>By default, bar selections looks horrible (with rounded selections). To fix this I use <a target="_blank" href="https://github.com/horst3180/arc-theme">Arc</a> theme. But to install it you'll need to first install <a target="_blank" href="https://extensions.gnome.org/extension/19/user-themes/">User Themes</a> extension. </p>
<pre><code><span class="hljs-attribute">sudo</span> apt install arc-theme
</code></pre><p>Then open <code>Tweaks</code> app "Appearance" section and set "Shell" to "Arc":</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640674493325/Eg_fLDFl0Y.png" alt="Screenshot from 2021-12-28 13-54-36.png" /></p>
<p>NOTE: it is also highly advised to change "Applications" theme (for example to "Adwaita") to get rid of orange selections (blue selections are much more natural).</p>
<h3 id="heading-multi-monitor">Multi-monitor</h3>
<p>To show different toolbars on each monitor (separate applications in toolbar) open dash-to-panel extension options "Behavior" tab:</p>
<ul>
<li>Disable "Show favorite applications on secondary panels"</li>
<li>Enable "Isolate monitors"</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643179031940/8MceMjYuy.png" alt="Screenshot from 2022-01-26 13-36-51.png" /></p>
<p>Also, make sure that on  "Position" tab "Display panels on all monitors" is enabled</p>
<h3 id="heading-different-wallpaper-on-login">Different wallpaper on login</h3>
<p>Install <a target="_blank" href="https://extensions.gnome.org/extension/1476/unlock-dialog-background/">Lock screen background</a> extension and set custom background in its options.</p>
<h3 id="heading-world-clocks">World clocks</h3>
<p>If you need to see multiple clocks (if you work with multiple time zones) install "Clocks" from "Ubuntu Software".</p>
<p>All configured additional clocks would appear in the main toolbar popup (when clicking on time).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640678168413/aEPhSZ2iF.png" alt="time.png" /></p>
<h3 id="heading-mulit-line-date">Mulit-line date</h3>
<p><a target="_blank" href="https://extensions.gnome.org/extension/4655/date-menu-formatter/">Date Menu Formatter</a> could help with tray date configuration and even do it in windows-style two-lines fashion</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641638245014/U2EXSgiPZ.png" alt="date.png" /></p>
<h3 id="heading-time-only-in-tray">Time only in tray</h3>
<p>By default, a "day of month" is shown in tray near time. To remove it use "Tweaks":</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640677649004/eFWSHO104.png" alt="Screenshot from 2021-12-28 14-46-58.png" /></p>
<p>Under "Top Bar" section disable "Date" switcher and "day of month" part would disappear.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640678308230/JEWjwL70C.png" alt="time_only.png" /></p>
<h3 id="heading-weather">Weather</h3>
<p>First, install the "Weather" app in "Ubuntu Software". Run it and add location.</p>
<p>Then install <a target="_blank" href="https://extensions.gnome.org/extension/1380/weather-in-the-clock/">Weather in The Clock</a> extension.</p>
<p>Weather icon will appear in tray and weather widget in the main popup (when clicking on date) and by clicking on this widget, the weather app would launch.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640675791867/rW-Pf4EHg.png" alt="weather.png" /></p>
<h3 id="heading-system-monitor">System monitor</h3>
<p>It is always good to see what happens under the hood. I prefer to see CPU/memory/network and CPU temperature in the tray:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640678569380/ycjrFcFA6.png" alt="metrics.png" /></p>
<p>Install "System Monitor" from "Ubuntu store".</p>
<p>And then install the extension:</p>
<pre><code>sudo apt install gnome<span class="hljs-operator">-</span>shell<span class="hljs-operator">-</span>extension<span class="hljs-operator">-</span>system<span class="hljs-operator">-</span>monitor
</code></pre><p>WARNING: it is important to install it like this and not from <a target="_blank" href="https://extensions.gnome.org/extension/120/system-monitor/">the website</a> because otherwise, it would not work.</p>
<p>System restart would be required.</p>
<p>After that configure graphs you need from extensions options (click on graph - "Preferences...")</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640676213811/VkReFI0aj.png" alt="Screenshot from 2021-12-28 14-23-08.png" /></p>
<p>Mark "Display" all graphs you want to see. Also, reduce "Graph Width" (to 50) for each graph.</p>
<h3 id="heading-terminal">Terminal</h3>
<p>"Quake-style" terminal (rising from above by hitting F12) is very handy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640676510658/x0-WGPv8Q.png" alt="Screenshot from 2021-12-28 14-27-02.png" /></p>
<p>Install <a target="_blank" href="https://extensions.gnome.org/extension/3780/ddterm/">ddterm</a> extension.</p>
<p>The tray icon may be removed in options.</p>
<p>NOTE: it is also handy to install <a target="_blank" href="https://dev.to/xeroxism/how-to-install-terminator-a-linux-terminal-emulator-on-steroids-1m3h">terminator</a> - replacement for the usual terminal which can split into multiple sections.</p>
<h2 id="heading-safe-eyes">Safe eyes</h2>
<p>Last but not least: small utility <a target="_blank" href="https://slgobinath.github.io/SafeEyes/">SafeEyes</a>.</p>
<p>All it does is show you a black screen every 15 minutes and force you to do some eye exercises.</p>
<p>It drives me mad sometimes, but it works very well. And it helps, especially with the "dry eyes".</p>
<pre><code>sudo add<span class="hljs-operator">-</span>apt<span class="hljs-operator">-</span>repository ppa:slgobinath<span class="hljs-operator">/</span>safeeyes
sudo apt update
sudo apt install safeeyes
</code></pre>]]></content:encoded></item><item><title><![CDATA[Ubuntu 21.04 (and up) VNC access to a locked session]]></title><description><![CDATA[The problem: since Ubuntu 21.04 you can use VNC connection ONLY with an ongoing session (it is a pure screen sharing feature now) and you'll not be able to connect to a locked computer.

Ubuntu 21.04 uses Wayland instead of x11 by default, but there ...]]></description><link>https://blog.vyarus.ru/ubuntu-2104-and-up-vnc-access-to-a-locked-session</link><guid isPermaLink="true">https://blog.vyarus.ru/ubuntu-2104-and-up-vnc-access-to-a-locked-session</guid><category><![CDATA[Ubuntu]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Fri, 24 Dec 2021 10:24:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640339748030/rWq55pUr0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>The problem</strong>: since Ubuntu 21.04 you can use VNC connection ONLY with an ongoing session (it is a pure screen sharing feature now) and you'll not be able to connect to a locked computer.</p>
</blockquote>
<p>Ubuntu 21.04 uses Wayland instead of x11 by default, but there are still no completely compatible VNC servers.</p>
<p>There is only a way to connect to the currently active session with <code>gnome-remote-desktop</code> (installed by default now instead of vino), but not to a locked session.</p>
<p>Note that <code>Settings/Sharing</code> settings configure now <code>gnome-remote-desktop</code> instead of vino!</p>
<p>To be able to login to locked session you'll need to restore vino. First remove <code>gnome-remote-desktop</code> (because even if we install vino, the system would still use gnome-remote-desktop):</p>
<pre><code>sudo apt remove gnome<span class="hljs-operator">-</span>remote<span class="hljs-operator">-</span>desktop
</code></pre><p>(after removing, screen sharing settings would disappear - that's normal!)</p>
<p>Install vino:</p>
<pre><code><span class="hljs-attribute">sudo</span> apt install vino
</code></pre><p>But vino wouldn't start automatically so we need to manually add it to autostart:</p>
<pre><code>cd <span class="hljs-operator">~</span><span class="hljs-operator">/</span>.config/autostart<span class="hljs-operator">/</span>
vim vino<span class="hljs-operator">-</span>server.desktop
</code></pre><p>autostart directory may not exist, in this case, create it first:</p>
<pre><code>mkdir <span class="hljs-operator">~</span><span class="hljs-operator">/</span>.config/autostart<span class="hljs-operator">/</span>
</code></pre><p>With the following content:</p>
<pre><code>[Desktop Entry]
Type<span class="hljs-operator">=</span>Application
Exec<span class="hljs-operator">=</span><span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>lib<span class="hljs-operator">/</span>vino<span class="hljs-operator">/</span>vino<span class="hljs-operator">-</span>server
Hidden<span class="hljs-operator">=</span><span class="hljs-literal">false</span>
X<span class="hljs-operator">-</span>MATE<span class="hljs-operator">-</span>Autostart<span class="hljs-operator">-</span>enabled<span class="hljs-operator">=</span><span class="hljs-literal">true</span>
Name[ru]<span class="hljs-operator">=</span>Vino Server
Name<span class="hljs-operator">=</span>Vino Server
Comment[ru]<span class="hljs-operator">=</span>
Comment<span class="hljs-operator">=</span>
</code></pre><p>NOTE: the same configuration could be performed through UI: run <code>Startup applications</code> tool and add a new record there.</p>
<p>With this entry, vino server would start on the first login, so when you lock the session you would still be able to connect. But you'll <strong>not</strong> be able to connect after the computer restart (first manual login is required).</p>
<p>After that configure vino:</p>
<pre><code>gsettings set org.gnome.Vino prompt<span class="hljs-operator">-</span>enabled <span class="hljs-literal">false</span> 
gsettings set org.gnome.Vino vnc<span class="hljs-operator">-</span>password $(echo <span class="hljs-operator">-</span>n <span class="hljs-string">"mypassword"</span><span class="hljs-operator">|</span>base64)
</code></pre><p>This should be enough, but make sure that you have all the following:</p>
<pre><code>dconf dump <span class="hljs-operator">/</span>org<span class="hljs-operator">/</span>gnome<span class="hljs-operator">/</span>desktop<span class="hljs-operator">/</span>remote<span class="hljs-operator">-</span>access<span class="hljs-operator">/</span>
[<span class="hljs-operator">/</span>]
authentication<span class="hljs-operator">-</span>methods<span class="hljs-operator">=</span>[<span class="hljs-string">'vnc'</span>]
lock<span class="hljs-operator">-</span>screen<span class="hljs-operator">-</span>on<span class="hljs-operator">-</span>disconnect<span class="hljs-operator">=</span><span class="hljs-literal">true</span>
prompt<span class="hljs-operator">-</span>enabled<span class="hljs-operator">=</span><span class="hljs-literal">false</span>
<span class="hljs-built_in">require</span><span class="hljs-operator">-</span>encryption<span class="hljs-operator">=</span><span class="hljs-literal">false</span>
vnc<span class="hljs-operator">-</span>password<span class="hljs-operator">=</span><span class="hljs-string">'yourpasswordhex'</span>
</code></pre><p>Note that <code>lock-screen-on-disconnect</code> is optional: without it, you'll have to manually lock the computer before disconnection (otherwise it would remain unlocked).</p>
<p>To simplify configuration you can use <code>dconf-editor</code>: install it from <code>Ubuntu software</code>. Open dconf-editor and go to <code>org/gnome/desktop/remote-access/</code>. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640339748030/rWq55pUr0.png" alt="Screenshot from 2021-12-24 16-55-09.png" /></p>
<p>NOTE: To change password value you'll need to click on <code>vnc-password</code> (last option) and make sure <code>use-default-value</code> is disabled. The <code>custom value</code> must contain the hex form of your password. To verify password hex use:</p>
<pre><code>echo <span class="hljs-operator">-</span>n <span class="hljs-string">"yourpassword"</span><span class="hljs-operator">|</span>base64
</code></pre><p>Now restart, <strong>but don't log in</strong>!</p>
<p>Vino can't work with wayland and so you'll need to switch session back to x11: on the login screen at the bottom right corner, there is a switcher to use x11 instead of Wayland. Set "Ubuntu on xorg" and log in (the setting should be remembered so next time you'll log in again with x11 - no need to switch every time). </p>
<p>Now lock (not log out!) computer and try connecting with a vnc client (for example, using <a target="_blank" href="https://remmina.org/">remmina</a> on linux and <a target="_blank" href="https://mobaxterm.mobatek.net/">mobaXterm</a> on windows).</p>
]]></content:encoded></item><item><title><![CDATA[Mkdocs Gradle plugin release]]></title><description><![CDATA[A new version of  mkdocs gradle plugin  just released featuring:

Documentation version selector
Dark theme switcher
Documentation aliases

Now generated docs could look like this:

Some time ago mkdocs-material introduced version switcher, but it re...]]></description><link>https://blog.vyarus.ru/mkdocs-gradle-plugin-release</link><guid isPermaLink="true">https://blog.vyarus.ru/mkdocs-gradle-plugin-release</guid><category><![CDATA[Java]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Wed, 08 Dec 2021 12:43:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1638964343667/iewTD2n_I.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A new version of  <a target="_blank" href="https://www.mkdocs.org/">mkdocs</a> gradle plugin <a target="_blank" href="https://github.com/xvik/gradle-mkdocs-plugin/releases/tag/2.2.0"> just released</a> featuring:</p>
<ul>
<li>Documentation version selector</li>
<li>Dark theme switcher</li>
<li>Documentation aliases</li>
</ul>
<p>Now generated docs could look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1638964343667/iewTD2n_I.png" alt="mkdocs2.png" /></p>
<p>Some time ago mkdocs-material <a target="_blank" href="https://squidfunk.github.io/mkdocs-material/setup/setting-up-versioning/#versioning">introduced version switcher</a>, but it requires <a target="_blank" href="https://github.com/jimporter/mike">mike</a> tool usage for publication. That was a problem because the plugin uses its own custom publication mechanism (much easily customizable from gradle).</p>
<p>But, it appears, all that mkdocs-material needs is just a <a target="_blank" href="https://xvik.github.io/gradle-mkdocs-plugin/versions.json">versions.json</a> file, describing all available versions. The new plugin version now always generates such file (based on gh-pages repository folders).</p>
<p>The interesting moment in the <code>versions.json</code> generation was versions sorting: there are no pre-defined patterns of possible versions and so versions should be sorted as strings. The natural order is not a good option because it does not count numbers well: 1.10, 1.11, 1.9. The solution was found in not (yet) accepted JDK improvement:  <a target="_blank" href="https://bugs.openjdk.java.net/browse/JDK-8134512">JDK-8134512</a> (actual JDK extension <a target="_blank" href="https://bugs.openjdk.java.net/browse/JDK-8213167">JDK-8213167</a> and <a target="_blank" href="http://cr.openjdk.java.net/~igerasim/8134512/04/webrev/index.html">the code itself</a>). Of course, this solution is still not ideal: for example, it always prioritizes SHAPSHOT or RC versions over releases (1.0-rc.1 &gt; 1.0 due to length), but still, the result is good enough.</p>
<p>Another aspect, missing in the publication mechanism, was documentation aliases.  The most common example is "latest" alias always pointing to the latest documentation. With it, users could use links <strong>always</strong> pointing to the most recent version. For example:</p>
<p>without an alias, a user could use only: <code>http://my-url/1.1.0/some-file.html</code></p>
<p>and with alias: <code>http://my-url/latest/some-file.html</code></p>
<p>To see it "in action", open <a target="_blank" href="https://xvik.github.io/gradle-mkdocs-plugin/">root documentation link</a> and it will redirect to the latest folder.</p>
<p>There are more useful applications for aliases: for example, documentation for developing version could be aliased as <code>dev</code> or alias could lead to the latest major version like <code>2.x</code> (again, could be handy for user links).</p>
]]></content:encoded></item><item><title><![CDATA[YAML confg migration tool]]></title><description><![CDATA[Nowadays YAML configs are everywhere. And often such configs must be updated over time. In many cases, it is possible to simply override the entire config file, but what if not? 
Here is the way for updating YAML configs preserving comments and all c...]]></description><link>https://blog.vyarus.ru/yaml-confg-migration-tool</link><guid isPermaLink="true">https://blog.vyarus.ru/yaml-confg-migration-tool</guid><category><![CDATA[Java]]></category><category><![CDATA[YAML]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Sat, 11 Sep 2021 08:39:17 GMT</pubDate><content:encoded><![CDATA[<p>Nowadays YAML configs are everywhere. And often such configs must be updated over time. In many cases, it is possible to simply override the entire config file, but what if not? </p>
<p>Here is the way for updating YAML configs <strong>preserving comments</strong> and all current values.</p>
<h2 id="preparing-the-tool">Preparing the tool</h2>
<p><a target="_blank" href="https://github.com/xvik/yaml-updater/">The tool</a> could be used in different ways, but I'll show the most universal <a target="_blank" href="https://github.com/xvik/yaml-updater/tree/master/yaml-config-updater-cli">command line</a> usage.</p>
<p><a target="_blank" href="https://repo1.maven.org/maven2/ru/vyarus/yaml-config-updater-cli/1.2.0/">Download</a> yaml-config-updater-cli-1.2.0-all.jar from maven central. </p>
<pre><code><span class="hljs-attribute">wget</span> https://repo<span class="hljs-number">1</span>.maven.org/maven<span class="hljs-number">2</span>/ru/vyarus/yaml-config-updater-cli/<span class="hljs-number">1</span>.<span class="hljs-number">2</span>.<span class="hljs-number">0</span>/yaml-config-updater-cli-<span class="hljs-number">1</span>.<span class="hljs-number">2</span>.<span class="hljs-number">0</span>-<span class="hljs-literal">all</span>.jar
</code></pre><p>Java 8 or above <a target="_blank" href="https://jdk.dev/download/">must be installed</a> in the target environment.</p>
<h2 id="adding-missed-properties">Adding missed properties</h2>
<p>Suppose your config is:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">section:</span>
    <span class="hljs-comment"># Important description</span>
    <span class="hljs-comment"># Value was manually updated (must be preserved)</span>
    <span class="hljs-attr">prop1:</span> <span class="hljs-number">12</span>
</code></pre>
<p>In the new version, new property was added:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">section:</span>
    <span class="hljs-comment"># Important description</span>
    <span class="hljs-attr">prop1:</span>  <span class="hljs-comment"># value must be put manually after installation</span>

    <span class="hljs-comment"># New description</span>
    <span class="hljs-attr">prop2:</span> <span class="hljs-number">10</span>
</code></pre>
<p>Performing migration:</p>
<pre><code><span class="hljs-selector-tag">java</span> <span class="hljs-selector-tag">-jar</span> <span class="hljs-selector-tag">yaml-config-updater-cli-1</span><span class="hljs-selector-class">.2</span><span class="hljs-selector-class">.0-all</span><span class="hljs-selector-class">.jar</span> <span class="hljs-selector-tag">config</span><span class="hljs-selector-class">.yml</span> <span class="hljs-selector-tag">update</span><span class="hljs-selector-class">.yml</span>
</code></pre><p>Output report:</p>
<pre><code><span class="hljs-attr">Updating configuration:</span> <span class="hljs-string">/home/user/test/config.yml</span>

<span class="hljs-attr">Configuration:</span> <span class="hljs-string">/home/user/test/config.yml</span> <span class="hljs-string">(105</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">5</span> <span class="hljs-string">lines)</span>
<span class="hljs-string">Updated</span> <span class="hljs-string">from</span> <span class="hljs-string">source</span> <span class="hljs-string">of</span> <span class="hljs-number">137</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">6</span> <span class="hljs-string">lines</span>
<span class="hljs-string">Resulted</span> <span class="hljs-string">in</span> <span class="hljs-number">93</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">7</span> <span class="hljs-string">lines</span>

    <span class="hljs-attr">Added from new file:</span>
        <span class="hljs-string">section/prop2</span>                            <span class="hljs-number">6</span>  <span class="hljs-string">|</span> <span class="hljs-attr">prop2:</span> <span class="hljs-number">10</span>

    <span class="hljs-attr">Backup created:</span> <span class="hljs-string">config.yml.20210911140958</span>
</code></pre><p>As a result, the new property would be added to the current config:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">section:</span>
    <span class="hljs-comment"># Important description</span>
    <span class="hljs-attr">prop1:</span> <span class="hljs-number">12</span>

    <span class="hljs-comment"># New description</span>
    <span class="hljs-attr">prop2:</span> <span class="hljs-number">10</span>
</code></pre>
<p>Note that the <code>prop1</code> comment was also changed, as comment from updating file may contain important information updates. Only in-line comments (after value) survive merging.</p>
<p>If you don't trust the report you can easily build diff with the created backup file (original config):</p>
<pre><code><span class="hljs-selector-tag">diff</span> <span class="hljs-selector-tag">config</span><span class="hljs-selector-class">.yml</span><span class="hljs-selector-class">.20210911140958</span> <span class="hljs-selector-tag">config</span><span class="hljs-selector-class">.yml</span>
5<span class="hljs-selector-tag">a6</span>,8
&gt;     # <span class="hljs-selector-tag">New</span> <span class="hljs-selector-tag">description</span>
&gt;     <span class="hljs-selector-tag">prop2</span>: 10
&gt;
</code></pre><h2 id="the-trust">The trust</h2>
<p>Good question: why should you trust the tool?<br />The tool is very young and most likely there are still bugs and uncovered cases.</p>
<p>And still, I can guarantee that the resulted file would be correct:</p>
<ul>
<li>Internally, time-prooved <a target="_blank" href="https://bitbucket.org/asomov/snakeyaml/src/master/">snakeyaml</a> parser used for all files validation. This way "comments" parser can be sure to always work with the correct YAML.</li>
<li>But that's not all. Parsed trees from comments and snakeyaml parsers are compared to eagerly detect incorrect parsing (self-validation)</li>
<li>Merge result is read by snakeyaml to make sure the file is correct. </li>
<li>Merged file values are carefully checked: all old values must be preserved and all new values added (using snakyaml trees to operate exact values).</li>
</ul>
<p>So you can be sure that the resulted config would be correct. If anything goes wrong, the configuration file would not be touched at all.</p>
<h2 id="removing-values">Removing values</h2>
<p>Adding new properties is not the only case: some properties might be removed in the actual configuration.</p>
<p>By default, the tool would not remove properties, so the merged file would still contain them.
In order to remove a property, you must explicitly specify it.</p>
<p>For example, suppose in config:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">prop1:</span> <span class="hljs-number">1</span>

<span class="hljs-attr">sub:</span>
    <span class="hljs-attr">foo:</span> <span class="hljs-number">2</span>
    <span class="hljs-attr">bar:</span> <span class="hljs-number">3</span>
</code></pre>
<p><code>prop1</code> and <code>foo</code> are no longer needed.</p>
<pre><code><span class="hljs-selector-tag">java</span> <span class="hljs-selector-tag">-jar</span> <span class="hljs-selector-tag">yaml-config-updater-cli-1</span><span class="hljs-selector-class">.2</span><span class="hljs-selector-class">.0-all</span><span class="hljs-selector-class">.jar</span> <span class="hljs-selector-tag">config</span><span class="hljs-selector-class">.yml</span> <span class="hljs-selector-tag">update</span><span class="hljs-selector-class">.yml</span> <span class="hljs-selector-tag">-d</span> <span class="hljs-selector-tag">prop1</span> <span class="hljs-selector-tag">sub</span><span class="hljs-selector-class">.foo</span>
</code></pre><pre><code><span class="hljs-attr">Updating configuration:</span> <span class="hljs-string">/home/user/test/config.yml</span>

<span class="hljs-attr">Configuration:</span> <span class="hljs-string">/home/user/test/config.yml</span> <span class="hljs-string">(36</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">6</span> <span class="hljs-string">lines)</span>
<span class="hljs-string">Updated</span> <span class="hljs-string">from</span> <span class="hljs-string">source</span> <span class="hljs-string">of</span> <span class="hljs-number">11</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">2</span> <span class="hljs-string">lines</span>
<span class="hljs-string">Resulted</span> <span class="hljs-string">in</span> <span class="hljs-number">18</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">4</span> <span class="hljs-string">lines</span>

    <span class="hljs-attr">Removed from old file:</span>
        <span class="hljs-string">prop1</span>                                    <span class="hljs-number">1</span>  <span class="hljs-string">|</span> <span class="hljs-attr">prop1:</span> <span class="hljs-number">1</span>
        <span class="hljs-string">sub/foo</span>                                  <span class="hljs-number">4</span>  <span class="hljs-string">|</span> <span class="hljs-attr">foo:</span> <span class="hljs-number">2</span>

    <span class="hljs-attr">Backup created:</span> <span class="hljs-string">config.yml.20210911144139</span>
</code></pre><p>This may be also used for replacing values: removed property would be replaced with a property from updating config with the default value.</p>
<p>You can remove entire sub-trees this way.</p>
<h2 id="config-template">Config-template</h2>
<p>Another common need is configuration "adoption" for the target environment. In this case, updating configuration become "a template" with placeholders to replace by environment-specific values:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">prop1:</span> <span class="hljs-comment">#{one}</span>
<span class="hljs-attr">prop2:</span> <span class="hljs-comment">#{two}</span>
<span class="hljs-attr">prop3:</span> <span class="hljs-comment">#{three}</span>
</code></pre>
<pre><code><span class="hljs-attribute">java</span> -jar yaml-config-updater-cli-<span class="hljs-number">1</span>.<span class="hljs-number">2</span>.<span class="hljs-number">0</span>-<span class="hljs-literal">all</span>.jar config.yml update.yml -e one=<span class="hljs-number">1</span> two=<span class="hljs-number">2</span>
</code></pre><p>Also, this time, updating configuration does not exist yet (first installation case).</p>
<pre><code><span class="hljs-attr">Updating configuration:</span> <span class="hljs-string">/home/user/test/config.yml</span> 

<span class="hljs-attr">Not exising configuration:</span> <span class="hljs-string">/home/user/test/config.yml</span> 
<span class="hljs-string">Updated</span> <span class="hljs-string">from</span> <span class="hljs-string">source</span> <span class="hljs-string">of</span> <span class="hljs-number">33</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">3</span> <span class="hljs-string">lines</span>
<span class="hljs-string">Resulted</span> <span class="hljs-string">in</span> <span class="hljs-number">34</span> <span class="hljs-string">bytes,</span> <span class="hljs-number">3</span> <span class="hljs-string">lines</span>

    <span class="hljs-attr">Applied variables:</span>
        <span class="hljs-string">two</span>                       <span class="hljs-string">=</span> <span class="hljs-number">2</span>
        <span class="hljs-string">one</span>                       <span class="hljs-string">=</span> <span class="hljs-number">1</span>

    <span class="hljs-string">New</span> <span class="hljs-string">configuration</span> <span class="hljs-string">copied</span> <span class="hljs-string">as-is</span>
</code></pre><p>The resulted config would be:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">prop1:</span> <span class="hljs-number">1</span>
<span class="hljs-attr">prop2:</span> <span class="hljs-number">2</span>
<span class="hljs-attr">prop3:</span> <span class="hljs-comment">#{three}</span>
</code></pre>
<p>Note that the third variable was not processed (as the value was not specified), but it is not a problem as any YAML parser would treat this placeholder as an in-line comment.</p>
<p>Environment variables might also be specified in a property file (and multiple files could be specified). By default, system environment variables are all provided (use <code>-i</code> option to see the list of all available variables during processing).</p>
<h2 id="links">Links</h2>
<p>This was just a brief introduction. More info could be found in github repository:</p>
<ul>
<li><a target="_blank" href="https://github.com/xvik/yaml-updater">Project root</a></li>
<li><a target="_blank" href="https://github.com/xvik/yaml-updater/tree/master/yaml-config-updater">API module</a></li>
<li><a target="_blank" href="https://github.com/xvik/yaml-updater/tree/master/yaml-config-updater-cli">CLI tool</a></li>
<li><a target="_blank" href="https://github.com/xvik/yaml-updater/tree/master/dropwizard-config-updater">Dropwizard module</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Guice-validator bugfix release]]></title><description><![CDATA[Guice-validator has been released for both javax.validation and jakarta.validation branches:

2.0.1 for hiberante-validator 6.x
3.0.1 for hiberante-validator 7.x

Both versions depend on guice 5 now, but are still compatible with guice 4 (just exclud...]]></description><link>https://blog.vyarus.ru/guice-validator-bugfix-release</link><guid isPermaLink="true">https://blog.vyarus.ru/guice-validator-bugfix-release</guid><category><![CDATA[Java]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Thu, 09 Sep 2021 07:06:33 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/xvik/guice-validator">Guice-validator</a> has been released for both <code>javax.validation</code> and <code>jakarta.validation</code> branches:</p>
<ul>
<li><a target="_blank" href="https://github.com/xvik/guice-validator/releases/tag/2.0.1">2.0.1</a> for hiberante-validator 6.x</li>
<li><a target="_blank" href="https://github.com/xvik/guice-validator/releases/tag/3.0.1">3.0.1</a> for hiberante-validator 7.x</li>
</ul>
<p>Both versions depend on guice 5 now, but are still compatible with guice 4 (just exclude transitive dep to downgrade).</p>
<p>The main reason for the release was a bug leading to manually specified <code>Default</code> group ignoring in <a target="_blank" href="https://github.com/xvik/guice-validator#default-group-specifics">strict groups mode</a>:</p>
<pre><code class="lang-java"><span class="hljs-keyword">new</span> ValidationModule().strictGroupsDeclaration()
</code></pre>
<p>For example, </p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Service</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">method</span><span class="hljs-params">(<span class="hljs-meta">@NotNull(groups = CustomGroup.class)</span> String arg1, 
                                        <span class="hljs-meta">@NotNull</span> String arg2 {
         ....
    }
}</span></span>
</code></pre>
<p>When  user manually specify <code>Default</code> group, e.g. like this:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Inject</span> ValidationContext context;
<span class="hljs-meta">@Inject</span> Service service;

context.doWithGroups(() -&gt; {            
                service.method(<span class="hljs-string">"foo"</span>, <span class="hljs-keyword">null</span>);
        }, Default.class, CustomGroup.class);
</code></pre>
<p>It was ignored and the call did not fail with a validation error (only in strict groups mode!).</p>
<p>I doubt anyone was ever affected by this, but just in case.</p>
]]></content:encoded></item><item><title><![CDATA[Javassist and JDK 16]]></title><description><![CDATA[As you may know, in JDK 16 illegal access become denied, so instead of warning you'll get an error like this:
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(j...]]></description><link>https://blog.vyarus.ru/javassist-and-jdk-16</link><guid isPermaLink="true">https://blog.vyarus.ru/javassist-and-jdk-16</guid><category><![CDATA[Java]]></category><dc:creator><![CDATA[Vyacheslav Rusakov]]></dc:creator><pubDate>Tue, 07 Sep 2021 05:04:27 GMT</pubDate><content:encoded><![CDATA[<p>As you may know, in JDK 16 illegal access become denied, so instead of warning you'll get an error like this:</p>
<pre><code><span class="hljs-selector-tag">Caused</span> <span class="hljs-selector-tag">by</span>: <span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.lang</span><span class="hljs-selector-class">.reflect</span><span class="hljs-selector-class">.InaccessibleObjectException</span>: <span class="hljs-selector-tag">Unable</span> <span class="hljs-selector-tag">to</span> <span class="hljs-selector-tag">make</span> <span class="hljs-selector-tag">protected</span> <span class="hljs-selector-tag">final</span> <span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.lang</span><span class="hljs-selector-class">.Class</span> <span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.lang</span><span class="hljs-selector-class">.ClassLoader</span><span class="hljs-selector-class">.defineClass</span>(<span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.lang</span><span class="hljs-selector-class">.String</span>,<span class="hljs-selector-tag">byte</span><span class="hljs-selector-attr">[]</span>,<span class="hljs-selector-tag">int</span>,<span class="hljs-selector-tag">int</span>,<span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.security</span><span class="hljs-selector-class">.ProtectionDomain</span>) <span class="hljs-selector-tag">throws</span> <span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.lang</span><span class="hljs-selector-class">.ClassFormatError</span> <span class="hljs-selector-tag">accessible</span>: <span class="hljs-selector-tag">module</span> <span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.base</span> <span class="hljs-selector-tag">does</span> <span class="hljs-selector-tag">not</span> "<span class="hljs-selector-tag">opens</span> <span class="hljs-selector-tag">java</span><span class="hljs-selector-class">.lang</span>" <span class="hljs-selector-tag">to</span> <span class="hljs-selector-tag">unnamed</span> <span class="hljs-selector-tag">module</span> <span class="hljs-keyword">@270421f5</span>
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.<span class="hljs-attribute">java:</span><span class="hljs-number">357</span>)
</code></pre><p>Of course, you can simply revert the previous behavior with the VM argument: <code>--illegal-access=permit</code>, but this option will be removed in JDK 17.</p>
<p>The solution could be found in <a target="_blank" href="https://github.com/jboss-javassist/javassist/issues/369">javassits issue</a>: you must not use <code>CtClass#toClass()</code> or <code>toClass(classloader)</code> anymore, instead use <code>toClass(neighborClass)</code>.</p>
<p>BUT this method will only work for java &gt;= 9, so if you need to support java 8, you'll have to use both methods:</p>
<pre><code class="lang-java"><span class="hljs-comment">// some class from classpath (neighbor) to inherit classloader and domain permissions from</span>
Class&lt;?&gt; baseClass = ...;
<span class="hljs-keyword">boolean</span> isJava8 = System.getProperty(<span class="hljs-string">"java.version"</span>).startsWith(<span class="hljs-string">"1.8"</span>);

CtClass generated = ...;
Class&lt;?&gt; result = isJava8 
         <span class="hljs-comment">// java 8 only</span>
         ? generated.toClass(baseClass.getClassLoader(), baseClass.getProtectionDomain())
         <span class="hljs-comment">// java 9 and above</span>
         : generated.toClass(baseClass);
</code></pre>
<p>The minimal required javassist is 3.24.</p>
]]></content:encoded></item></channel></rss>