Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4340ea3
Allow adding OFT plugins
kaklakariada Aug 21, 2026
fdefe14
Revert integration test
kaklakariada Aug 31, 2026
c81f83a
Merge branch 'main' into allow-adding-plugins
kaklakariada Aug 31, 2026
e5108bb
Add integration test for asciidoc plugin
kaklakariada Aug 31, 2026
99c1ab8
Add workaround for missing class in OFT
kaklakariada Aug 31, 2026
cd9eb16
Code cleanup
kaklakariada Aug 31, 2026
7712b7f
Improve child first class loader
kaklakariada Aug 31, 2026
26f38eb
Code cleanup
kaklakariada Sep 3, 2026
884bcb8
Fix classpath issues with OFT
kaklakariada Sep 4, 2026
50425bf
Fix missing javadoc comment
kaklakariada Sep 5, 2026
a8b7af1
Upgrade github actions
kaklakariada Sep 5, 2026
5a093b0
Suppress sonar warnings
kaklakariada Sep 5, 2026
8f11afb
Fix sonar warnings
kaklakariada Sep 5, 2026
36819f4
Merge remote-tracking branch 'origin/main' into allow-adding-plugins
kaklakariada Sep 20, 2026
c5b8223
Fix merge errors
kaklakariada Sep 24, 2026
f97db31
Upgrade asciidoc plugin
kaklakariada Sep 25, 2026
36c41c7
Fix issue with reporter factory
kaklakariada Sep 25, 2026
fbdde3d
Merge remote-tracking branch 'origin/main' into allow-adding-plugins
kaklakariada Sep 25, 2026
1f0e866
Upgrade gradle version
kaklakariada Sep 25, 2026
f2e1dc7
Add plan to simplify plugin loading
kaklakariada Sep 26, 2026
e901d64
Merge remote-tracking branch 'origin/main' into allow-adding-plugins
kaklakariada Oct 11, 2026
261a721
Revert classloader implementation
kaklakariada Oct 11, 2026
9c07f0b
Code cleanup
kaklakariada Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- [#77](https://github.com/itsallcode/openfasttrace-gradle/issues/77)
- Add support for OpenFastTrace plugin dependencies

## [3.4.0] - 2026-09-22

- Upgrade to OpenFastTrace [4.10.0](https://github.com/itsallcode/openfasttrace/releases/tag/4.10.0)
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ You can configure the following properties:
* `filteredArtifactTypes`: Use only the listed artifact types during tracing
* `filterWantedStatuses`: Import only specification items that have a status contained in the list of statuses. Possible values: `draft`, `proposed`, `approved`, `rejected`. See the [OFT user guide](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide/user_guide.md#filtering-by-status) for details.

### Using OpenFastTrace Plugins

OpenFastTrace extension plugins can be added with the `pluginDependencies` property. The dependencies are added to the classpath used by requirement collection and tracing:

```groovy
repositories {
mavenCentral()
}

requirementTracing {
pluginDependencies = ['org.itsallcode:openfasttrace-asciidoc-plugin:1.0.0']
}
```

These are OpenFastTrace extension plugins, not Gradle build plugins. Plugin JARs must provide the appropriate OpenFastTrace service descriptors and should not include a duplicate incompatible `openfasttrace-api` dependency. Plugin discovery remains additive to OpenFastTrace's built-in plugin directory.

### Configuring the Short Tag Importer

The short tag importer allows omitting artifact type and the covered artifact type. Optionally you can add a prefix to the item name, e.g. a common module name.
Expand Down
102 changes: 102 additions & 0 deletions docs/plugin-loading-refactor-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Plan: Simplify plugin loading by consuming the new OFT plugin API

Status: planned
Upstream issue: [itsallcode/openfasttrace#610 — Allow adding plugins via configuration](https://github.com/itsallcode/openfasttrace/issues/610)
Related: [openfasttrace-gradle#77 — Add support for OpenFastTrace plugin dependencies](https://github.com/itsallcode/openfasttrace-gradle/issues/77)

## Goal

Delete the Gradle plugin's custom class-loader machinery and let OpenFastTrace (OFT)
load plugin JARs directly, once OFT exposes a public API for passing plugin JAR paths
(issue #610). This assumes the upstream feature is available in an upcoming OFT release.

## Why this is possible

The current complexity exists only because OFT's plugin loading is not programmable:

- `ServiceLoaderFactory` is package-private and hardcodes `$HOME/.oft/plugins`.
- `InitializingServiceLoader.load(Class<T>, C)` is the only public entry point and
accepts no plugin locations.
- `ClassPathServiceLoader.filterOtherClassLoader` rejects any service whose
`getClass().getClassLoader()` differs from the origin's class loader.

Because the Gradle plugin cannot tell OFT about extra plugin JARs, it hijacks the
thread context class loader (TCCL) with a child-first loader. Due to the class-loader
identity filter above, it must additionally locate and copy OFT's *built-in* provider
JARs (importer/exporter/reporter factories) into that same child loader — otherwise
built-in reporting and importing silently disappear as soon as any plugin is configured.

OFT already implements this correctly for `$HOME/.oft/plugins` via `ServiceOrigin` +
its own `ChildFirstClassLoader`. Issue #610 exposes that mechanism to library users.

## Assumed upstream API (issue #610)

```java
Oft oft = new OftRunner(List.of(pluginJar1, pluginJar2));
```

Threaded internally through `ServiceFactory` → `ImporterFactoryLoader` /
`ExporterFactoryLoader` / `ReporterFactoryLoader` → `InitializingServiceLoader.load(
serviceType, context, pluginJars)` → `ServiceLoaderFactory`, which merges the paths as
an additional `ServiceOrigin.forJars(pluginJars)`. The existing no-arg `OftRunner()`
delegates with an empty list, so the change is backward compatible.

> Confirm the exact signature and release version before implementing. If the final API
> differs (e.g. plugin paths on `ImportSettings`/`ReportSettings` instead of the
> `OftRunner` constructor), adjust step 3 accordingly.

## Steps

1. **Bump the OFT version.** In `build.gradle`, set `oftVersion` to the first release
that contains the #610 API.
2. **Delete the class-loader package.** Remove
`src/main/java/org/itsallcode/openfasttrace/gradle/task/classloader/`:
- `OftPluginClassLoader.java`
- `ChildFirstClassLoader.java`
- `ParentClassLoader.java`
3. **Wire plugin files into `OftRunner`.**
- `CollectTask.collectRequirements()`: replace
`OftPluginClassLoader.runWithPlugins(pluginFiles, this::collectWithPlugins)` with a
direct call to `collectWithPlugins()`, and construct the runner with plugin paths.
- `TraceTask.trace()`: same for `traceWithPlugins()`.
- Build the plugin path list from the resolved `pluginFiles` `ConfigurableFileCollection`,
e.g. `pluginFiles.getFiles().stream().map(File::toPath).toList()`.
- Remove the now-unused `OftPluginClassLoader` imports.
4. **Update documentation.**
- `README.md` "Using OpenFastTrace Plugins": drop the class-loader/TCCL caveats and
note that OFT handles plugin isolation.
- `CHANGELOG.md`: add an entry under `[Unreleased]` referencing #610 and #77.
5. **Verify.**

## Files

| File | Change |
| --- | --- |
| `build.gradle` | Bump `oftVersion` |
| `src/main/java/.../task/CollectTask.java` | Use `new OftRunner(pluginPaths)`, drop wrapper |
| `src/main/java/.../task/TraceTask.java` | Use `new OftRunner(pluginPaths)`, drop wrapper |
| `src/main/java/.../task/classloader/*` | Delete (3 files) |
| `README.md` | Update plugin section |
| `CHANGELOG.md` | Add Unreleased entry |

## Verification

1. `./gradlew :test --tests org.itsallcode.openfasttrace.gradle.OpenFastTracePluginTest`
— in particular `testTraceExampleProjectWithPluginDependency`.
2. Run the `plugin-config` example and confirm both the built-in `plain` report **and**
the asciidoc plugin import work.
3. `./gradlew clean build --warning-mode fail -PenableConfigurationCache=true -PjavaVersion=25`
(per repo memory: local JDK is Java 25; use `-PjavaVersion=25`).

## Risks / notes

- **Cross-repo dependency.** This plan is blocked until the OFT release containing #610
is published. Do not merge the Gradle change before then.
- **API drift.** The assumed `OftRunner(List<Path>)` signature may change during upstream
review; re-check #610 before implementing.
- **Child-first semantics.** OFT's `ChildFirstClassLoader` is fully child-first (no
`api`/`core` parent-first exception, unlike the current Gradle loader). This is safe
because the plugin developer guide forbids bundling `openfasttrace-api`; confirm this
holds for the asciidoc plugin.
- **No fallback.** Per decision, the Gradle plugin will hard-require the new OFT version
rather than keeping the TCCL-based loader for older versions.
15 changes: 15 additions & 0 deletions example-projects/plugin-config/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
id "base"
id 'org.itsallcode.openfasttrace'
}

repositories {
mavenCentral()
}

requirementTracing {
failBuild = true
inputDirectories = files('doc', 'src')
reportFormat = 'plain'
pluginDependencies = ['org.itsallcode:openfasttrace-asciidoc-plugin:1.0.0']
}
6 changes: 6 additions & 0 deletions example-projects/plugin-config/doc/spec.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
== AsciiDoc Spec

[.specitem, oft-sid="dsn~asciidoc-exampleB~1", oft-needs="impl,test"]
=== Example AsciiDoc Requirement

Example AsciiDoc requirement
6 changes: 6 additions & 0 deletions example-projects/plugin-config/doc/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# MarkDown Tracing Example
`dsn~md-exampleA~1`

Example MarkDown requirement

Needs: impl, test
1 change: 1 addition & 0 deletions example-projects/plugin-config/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'plugin-config'
5 changes: 5 additions & 0 deletions example-projects/plugin-config/src/Source.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// [impl->dsn~md-exampleA~1]
// [impl->dsn~asciidoc-exampleB~1]
class Source
{
}
5 changes: 5 additions & 0 deletions example-projects/plugin-config/src/Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// [test->dsn~md-exampleA~1]
// [test->dsn~asciidoc-exampleB~1]
class Test
{
}
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ private static TaskProvider<CollectTask> createCollectTask(final Project rootPro
task.setGroup(TASK_GROUP_NAME);
task.setDescription("Collect requirements and generate specobject file");
task.getInputDirectories().set(getAllInputDirectories(rootProject.getAllprojects()));
task.getPluginFiles().from(
getPluginDependencies(rootProject, rootProject.getAllprojects()));
task.getOutputFile().set(
rootProject.getLayout().getBuildDirectory().file("reports/requirements.xml"));
task.getPathConfig().set(getPathConfig(rootProject.getAllprojects()));
Expand Down Expand Up @@ -109,6 +111,8 @@ private static void configureTracingTask(final Project rootProject,
task.getReportFormat().set(config.getReportFormat());
task.getImportedRequirements()
.from(getImportedRequirements(rootProject, rootProject.getAllprojects()));
task.getPluginFiles().from(
getPluginDependencies(rootProject, rootProject.getAllprojects()));
task.getFilteredArtifactTypes().set(config.getFilteredArtifactTypes());
task.getFilteredTags().set(config.getFilteredTags());
task.getFilterAcceptsItemsWithoutTag().set(config.getFilterAcceptsItemsWithoutTag());
Expand Down Expand Up @@ -173,6 +177,45 @@ private static Optional<Provider<Configuration>> getImportedRequirements(final P
return Optional.of(project.getConfigurations().named(CONFIG_NAME));
}

private static ConfigurableFileCollection getPluginDependencies(final Project rootProject,
final Set<Project> allProjects)
{
return rootProject.files(allProjects.stream()
.map(OpenFastTracePlugin::getPluginDependencies)
.flatMap(Optional::stream)
.toList());
}

private static Optional<Configuration> getPluginDependencies(final Project project)
{
final List<Object> dependencies = getConfig(project).getPluginDependencies().get();
if (dependencies.isEmpty())
{
return Optional.empty();
}
final String CONFIG_NAME = "oftPluginConfig";
return Optional.of(getOrCreateConfiguration(project, CONFIG_NAME, dependencies));
}

private static Configuration getOrCreateConfiguration(final Project project,
final String configurationName, final List<Object> dependencies)
{
final Configuration existingConfiguration = project.getConfigurations()
.findByName(configurationName);
if (existingConfiguration != null)
{
return existingConfiguration;
}

final Configuration configuration = project.getConfigurations().create(configurationName);
dependencies.forEach(dependency -> {
LOG.info("Adding dependency {} with configuration {} to project {}", dependency,
configurationName, project);
project.getDependencies().add(configurationName, dependency);
});
return configuration;
}

private static List<SerializableTagPathConfig> getPathConfig(final Set<Project> allProjects)
{
return allProjects.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class TracingConfig
private final ConfigurableFileCollection inputDirectories;
private final RegularFileProperty reportFile;
private final ListProperty<Object> importedRequirements;
private final ListProperty<Object> pluginDependencies;
private final SetProperty<String> filteredTags;
private final SetProperty<String> filteredArtifactTypes;
private final SetProperty<String> filterWantedStatuses;
Expand All @@ -35,7 +36,7 @@ public class TracingConfig

/**
* Creates a tracing configuration with the plugin defaults.
*
*
* @param project
* the Gradle project owning the configuration
*/
Expand All @@ -49,6 +50,7 @@ public TracingConfig(final Project project)
this.reportFormat = project.getObjects().property(String.class);
this.reportFormat.set(DEFAULT_REPORT_FORMAT);
this.importedRequirements = project.getObjects().listProperty(Object.class);
this.pluginDependencies = project.getObjects().listProperty(Object.class);
this.filteredTags = project.getObjects().setProperty(String.class);
this.filteredArtifactTypes = project.getObjects().setProperty(String.class);
this.filterAcceptsItemsWithoutTag = project.getObjects().property(Boolean.class);
Expand All @@ -62,7 +64,7 @@ public TracingConfig(final Project project)

/**
* Returns the report verbosity property.
*
*
* @return the verbosity property
*/
public Property<ReportVerbosity> getReportVerbosity()
Expand Down Expand Up @@ -121,6 +123,16 @@ public ListProperty<Object> getImportedRequirements()
return importedRequirements;
}

/**
* Returns the OpenFastTrace plugin dependencies.
*
* @return the plugin dependencies
*/
public ListProperty<Object> getPluginDependencies()
{
return pluginDependencies;
}

/**
* Returns the tags to include in tracing.
*
Expand Down Expand Up @@ -301,6 +313,17 @@ public void setImportedRequirements(final List<Object> importedRequirements)
this.importedRequirements.set(importedRequirements);
}

/**
* Sets the OpenFastTrace plugin dependencies.
*
* @param pluginDependencies
* dependencies to add to the OpenFastTrace plugin classpath
*/
public void setPluginDependencies(final List<Object> pluginDependencies)
{
this.pluginDependencies.set(pluginDependencies);
}

/**
* Sets the tags to include in tracing.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.stream.Stream;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.SetProperty;
Expand Down Expand Up @@ -35,6 +36,8 @@ public class CollectTask extends DefaultTask
@SuppressWarnings({ "this-escape" })
public final ListProperty<SerializableTagPathConfig> pathConfig = getProject().getObjects()
.listProperty(SerializableTagPathConfig.class);
@SuppressWarnings("this-escape")
private final ConfigurableFileCollection pluginFiles = getProject().files();

/** Creates the task. */
public CollectTask()
Expand Down Expand Up @@ -76,12 +79,23 @@ public ListProperty<SerializableTagPathConfig> getPathConfig()
return pathConfig;
}

/**
* Returns the OpenFastTrace plugin files.
*
* @return the plugin files
*/
@InputFiles
@PathSensitive(PathSensitivity.ABSOLUTE)
public ConfigurableFileCollection getPluginFiles()
{
return pluginFiles;
}

/** Collects specification items and writes the specobject file. */
@TaskAction
public void collectRequirements()
{
createReportOutputDir();

final Oft oft = new OftRunner();
final ImportSettings settings = getImportSettings();
getLogger().info("Importing from {} locations {} and {} path configurations: {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class TraceTask extends DefaultTask
private final Property<DetailsSectionDisplay> detailsSectionDisplay = getProject().getObjects()
.property(DetailsSectionDisplay.class);
private final ConfigurableFileCollection importedRequirements = getProject().files();
private final ConfigurableFileCollection pluginFiles = getProject().files();
private final SetProperty<String> filteredArtifactTypes = getProject().getObjects()
.setProperty(String.class);
private final SetProperty<String> filteredTags = getProject().getObjects()
Expand Down Expand Up @@ -123,6 +124,18 @@ public ConfigurableFileCollection getImportedRequirements()
return importedRequirements;
}

/**
* Returns the OpenFastTrace plugin files.
*
* @return the plugin files
*/
@InputFiles
@PathSensitive(PathSensitivity.ABSOLUTE)
public ConfigurableFileCollection getPluginFiles()
{
return pluginFiles;
}

/**
* Returns the artifact type filter.
*
Expand Down
Loading
Loading