Task properties overwritten

Greetings. I’m trying to write a plugin that has a few different tasks of the same type, but with slightly different behavior. I’m following this guide and I’ve created a standalone project for the plugin. I’m specifically trying to follow the section [“Mapping Extension Properties to Task Properties”]
(https://docs.gradle.org/current/userguide/custom_plugins.html#sec:mapping_extension_properties_to_task_properties) . The plugin seems to work, but it seems to me that the Task properties are shared per Task class, and not per instance. I’m sure I’m doing something wrong here, and I’d appreciate some guidance. My code looks like this:

class VersioningPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        def patchExt = project.extensions.create('semantic', VersioningPluginExtension, project)

        project.tasks.create('bumpPatch', PatchTask) {
            bumpLevel = patchExt.bumpLevel
            bumpLevel.set(BumpLevel.PATCH)
            tagPrefix = extension.tagPrefix
            version = extension.version
        }

        project.tasks.create('bumpMinor',  PatchTask) {
            bumpLevel = extension.bumpLevel
            bumpLevel.set(BumpLevel.MINOR)
            tagPrefix = extension.tagPrefix
            version = extension.version
        }

        project.tasks.create('bumpMajor', PatchTask) {
            bumpLevel = extension.bumpLevel
            bumpLevel.set(BumpLevel.MAJOR)
            tagPrefix = extension.tagPrefix
            version = extension.version
        }
    }
}

class VersioningPluginExtension {
    Property<BumpLevel> bumpLevel
    Property<String> version
    Property<String> tagPrefix

    VersioningPluginExtension(Project project) {
        bumpLevel = project.objects.property(BumpLevel)
        version = project.objects.property(String)
        tagPrefix = project.objects.property(String)
    }
}

@Slf4j
class PatchTask extends DefaultTask {
    // bump and version are properties that can be configured in an extension closure in build.gradle
    Property<BumpLevel> bumpLevel
    Property<String> version
    Property<String> tagPrefix

    // Bump the patch number of the release based on the latest git tag.
    @TaskAction
    String bumpVersion() {
        // If the property is set through build.gradle, then use it, otherwise use git tagging to figure it out.
        def prefix = tagPrefix && tagPrefix.isPresent() ? tagPrefix.get() : "*"
        def level = bumpLevel && bumpLevel.isPresent() ? bumpLevel.get() : BumpLevel.PATCH
        def ver = version && version.isPresent() ? version.get() : getTagVersion(prefix)
        SemVer bumpedVersion = new SemVer(ver).bump(level)
        log.debug("Bumping project version to $bumpedVersion")

        return bumpedVersion.toString()
    }

    // Use git to find the latest tag that matches X.Y.Z
    String getTagVersion(prefix) {
        def proc = ['sh', '-c', 'git tag -l --sort=-taggerdate \'${prefix}\' | head -1'].execute()
        log.debug("Searching for the latest tag using [$proc]")
        def result = proc.in.text.find(/\d+\.\d+\.\d+/)

        if (! result) {
            log.error("Unable to find latest tag!")
        }

        return result
    }
}

I have the three Task instances bumpPatch, bumpMinor, and bumpMajor, and I would like to be able to configure them separately, but to share the property names. I.e. I’d like to be able to use a per Task closure in build.gradle if this is possible:

semantic { 
    tagPrefix = ...
}

With my current approach, the properties that get set inside the various project.tasks.create(...) calls seem to be shared between the Task instances. How do I make sure they are per Task instance but with the same name? Or am I taking the wrong approach on this? Any insights would be helpful. Thanks.

Looking at the code again, I think I know what’s happening. The Task properties are simply referencing the extension instance properties, and there’s only one instance of the extension. Is there a way of copying the properties from the extension instance into the Task properties instead of simply referencing the extension properties? Or should I simply keep an extension instance per task?