Skip to content

feat: implement version pinning for JBang aliases#2433

Draft
maxandersen wants to merge 1 commit intojbangdev:mainfrom
maxandersen:version-pinning
Draft

feat: implement version pinning for JBang aliases#2433
maxandersen wants to merge 1 commit intojbangdev:mainfrom
maxandersen:version-pinning

Conversation

@maxandersen
Copy link
Copy Markdown
Collaborator

@maxandersen maxandersen commented Mar 25, 2026

Implements alias:version@catalog syntax to pin versions at invocation time without modifying catalog files.

Try and fix with the syntax suggested in #1979

Examples:

jbang picocli:4.7.0                    # GAV version replacement
jbang tool:v1.0@jbangdev               # Git URL branch replacement
jbang app:2.0@myorg/repo/main          # Catalog ref replacement

Implementation

Version replacement cascade:

  1. Property replacement (${jbang.app.version:default}) - takes precedence

As in, if alias has property replacement we will NOT do automagic replacement because
can't know user really want the replacement .

  1. Maven GAV replacement (group:artifact:versiongroup:artifact:newversion)
  2. Git URL replacement (GitHub/GitLab/Bitbucket branch/tag substitution)
  3. Catalog reference replacement (alias@org/repo/refalias@org/repo/version)
  4. Hard error if version specified but no pattern matches.

Did this with the thinking that if version replacement not possible its better we fail - but of course if jbang.app.version is included we 'll have to just trust the catalog writer.

For example:

{
  "aliases": {
    "quarkus": {
      "script-ref": "io.quarkus:quarkus-cli-${jbang.app.version:3.0.0}:3.7.0"
    }
  }
}

Running jbang quarkus:3.5.0 resolves to io.quarkus:quarkus-cli-3.5.0:3.7.0

Feedback Needed

  1. Hard error vs warning: Currently throws ExitException when version cannot be applied to unknown URL patterns. Is this the right behavior (least surprise) or should it warn and continue?

  2. GAV detection heuristic: Uses colon counting (2+ colons = GAV) and dot detection in first segment (groupId pattern). Edge cases where this might fail?

  3. Catalog reference version pinning: For registered catalog names (e.g., tool:1.0@mycatalog where mycatalog is a named catalog), currently errors since we can't modify the catalog URL. Should this work
    differently?

Documentation

Added complete "Version Pinning" section to docs/modules/ROOT/pages/alias_catalogs.adoc with syntax, examples, and all replacement strategies.

* Version requested by user via alias:version syntax. Transient because it's
* not stored in catalog files - only used at runtime for version resolution.
*/
public transient String requestedVersion;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not needed - just calculate on need to know basis.

Refactor alias version parsing/pinning to avoid side effects during lookup and
apply version/property logic in the resolver layer with proper context.

- Add AliasRef for side-effect-free parsing of alias:version@catalog
- Add AliasVersionPinner for property-first and pattern-based pinning
- Stop resolving properties inside Alias.resolve()
- Keep version pinning from propagating through alias chains
- Update docs examples and fix alias-chain behavior; format examples as tables
- Adjust tests to avoid network/WireMock dependencies

Made-with: Cursor
@maxandersen maxandersen marked this pull request as ready for review March 26, 2026 14:47
@maxandersen maxandersen requested a review from quintesse March 26, 2026 14:47
@maxandersen maxandersen marked this pull request as draft March 26, 2026 14:47
@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 26, 2026

@maxandersen , the logic that is required is described in the following Python/Java like pseudocode:

import org.apache.commons.text.StringSubstitutor

String scriptRef = "camel:4.17.0@apache/camel"

# alias object can be null.
# version is an empty string or a valid version string ("1.0.2")
(Alias alias, String version) = getAlias(scriptRef)

if not alias:
    return

# Set version to the "default-version" property
# if version is empty. "default-version" might
# not be defined in which case it is also set by
# default to the empty string.
if version.length() == 0:
    version = alias.defaultVersion()

if version.length() > 0:
    # replace all instances of "${version}" in string values 
    # inside the alias record with the value of version.toString()

    # Convert the alias object to its JSON representation
    jsonText0 = alias.toJson()

    # Create a version HashMap
    versionMap = {"version": version}

    # Replace all instances of "${version}" with version.toString()
    sub = StringSubstitutor(versionMap)
    jsonText1 = sub.replace(jsonText0)

    # Create a new alias object from the version expanded JSON text
    alias = Alias.fromJson(jsonText1)

# Continue using alias object

Using the following jbang-catalog.json as a reference. Property default-version might not be present, in which case its value would be the empty string when retrieved from the Alias object.

{
  "aliases": {
    "camel": {
      "default-version": "4.18.0",
      "script-ref": "dsl/camel-jbang/camel-jbang-main/dist/CamelJBang.java",
      "properties": {
        "camel.jbang.version": "${version}",
        "camel-kamelets.version": "${version}"
      }
    }
  }
}

@maxandersen
Copy link
Copy Markdown
Collaborator Author

maxandersen commented Mar 26, 2026

@wfouche I don't know or follow at all what that code is trying to do.
default-version nor ${version} is not something required in any of the conversations we had.

Please show explicit cases that you believe is failing with this PR?

like:

Given the following catalog:

These aliases should resolve as this:

a:1.0 -> ...

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 27, 2026

@maxandersen , after our meeting today, I have the missing context that I needed to understand the reasoning behind the approach you follow in the PR. I'll test this and see how well it works, and provide feedback.

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 27, 2026

Using the following jbang-catalog.json file

{
  "aliases" : {
    "junit" : {
      "script-ref" : "org.junit.platform:junit-platform-console-standalone:6.0.3",
      "description" : "Launch the JUnit Platform from the console"
    },
    "cli" : {
      "script-ref" : "org.junit.platform:junit-platform-console-standalone:6.0.3",
      "description" : "Launch the JUnit Platform from the console"
    }
  }
}

I ran commands:

$ ./build/install/jbang/bin/jbang run cli --version
JUnit Platform Console Launcher 6.0.3
JVM: 21.0.10 (Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.10+7-LTS)
OS: Linux 6.6.87.2-microsoft-standard-WSL2 amd64

$ ./build/install/jbang/bin/jbang run cli:6.0.2 --version
JUnit Platform Console Launcher 6.0.2
JVM: 21.0.10 (Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.10+7-LTS)
OS: Linux 6.6.87.2-microsoft-standard-WSL2 amd64

Versioning of GAVs works! However, this feels a bit unreal to me. :-)

I'll test a script next.

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 27, 2026

I've created three scripts:

$ jbang run hello/hello.java
Hello World
$ jbang run hello/1.0.0/hello.java
Hello World, 1.0.0
$ jbang run hello/1.0.1/hello.java
Hello World, 1.0.1

jbang-catalog.json

{
  "aliases" : {
    "h": {
      "script-ref": "./hello/${jbang.app.version:}/hello.java"
    }
  }
}

Testing versioning functionality

$ ../build/install/jbang/bin/jbang run h
Hello World

$ ../build/install/jbang/bin/jbang run h:1.0.0
Hello World, 1.0.0

@maxandersen , it works very well. I like it. All that I found that is missing, is that JBang do not pass property jbang.app.version on to the script using -Djbang.app.version=1.0.0 , we need this for the solution to be complete.

Updated test program:

///usr/bin/env jbang "$0" "$@" ; exit $?

import java.lang.System;

public class hello {
    public static void main(String... args) {
        System.out.println("Hello World, 1.0.0");
        String version = System.getProperty("jbang.app.version");
        if (version != null) {
            System.out.println("JBang App Version: " + version);
        } else {
            System.out.println("Property 'jbang.app.version' is not defined.");
        }
    }
}

I also tested with script-ref set to "./hello/${jbang.app.version:1.0.0}/hello.java"

$ ../build/install/jbang/bin/jbang run h
Hello World, 1.0.0

$ ../build/install/jbang/bin/jbang run h:1.0.1
Hello World, 1.0.1

The following alias could not be resolved:

    "camel": {
      "script-ref": "camel@apps/camel/${jbang.app.version:4.16.0}/jbang-catalog.json"
    }
$ ../build/install/jbang/bin/jbang run camel
[jbang] [ERROR] Unknown catalog 'apps/camel/${jbang.app.version:4.16.0}/jbang-catalog.json'

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 28, 2026

In summary, there are two issues to resolve:

  • jbang.app.version should be set in the app JVM environment.
  • support versioned script-refs pointing to jbang-catalog.json files

With these two issues resolved, I think we'll have a great and useful versioning solution.

@maxandersen
Copy link
Copy Markdown
Collaborator Author

  • jbang.app.version should be set in the app JVM environment.

Why/what is the usecase for the app to know what jbang.app.version was used on the command line? That is not really the business of jbang to tell an app what version it is ? There is zero guarantees that version relates to the application version.

I get it could be nice to have but it is not something to rely and thus not a fan of exposing it.

  • support versioned script-refs pointing to jbang-catalog.json files

I assume you mean this error:

../build/install/jbang/bin/jbang run camel
[jbang] [ERROR] Unknown catalog 'apps/camel/${jbang.app.version:4.16.0}/jbang-catalog.json'

can you give me a repo/branch with what you have setup because this direct pointing to a jbang-catalog.json seems irrelevant...I assume its more about that the jbang.app.version is not replaced in case of no alias version specified?

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 28, 2026

jbang.app.version should be set in the app JVM environment.

I've changed my mind. Don't set the property in the app JVM.

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 28, 2026

can you give me a repo/branch with what you have setup

https://github.com/wfouche/jbang/tree/dev/version-tests/tests

{
    "hw": {
      "script-ref": "hello@./apps/hello/1.0.0/jbang-catalog.json"
    },
    "hw2": {
      "script-ref": "hello@./apps/hello/${jbang.app.version:1.0.0}/jbang-catalog.json"
    }
}

Issue 1

hw is resolved and works, hw2 is not resolved.

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 28, 2026

Issue 2

    "camel": {
      "script-ref": "./CamelJBang.java"
    }

jbang run camel version works, jbang run camel:4.17.0 version fails with error message:

  • [jbang] [ERROR] Cannot apply version '4.17.0' to script-ref './CamelJBang.java'. No recognizable version pattern found. Supported patterns: Maven GAV, GitHub/GitLab/Bitbucket URLs, or use ${jbang.app.version:default} in the alias definition.
  • It is perfectly valid to have a script-ref for a versioned alias to be just "./CamelJBang.java".

@maxandersen
Copy link
Copy Markdown
Collaborator Author

'It is perfectly valid to have a script-ref for a versioned alias to be just "./CamelJBang.java".'

I dont see how. That is not a versioned alias. So no way to apply version Alias to it.

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 28, 2026

I dont see how. That is not a versioned alias. So no way to apply version Alias to it.

The "how" is that we just use the version number specified for the alias in jbang.app.version to find the correct //DEPS to load.

///usr/bin/env jbang "$0" "$@" ; exit $?

//JAVA 21+
//REPOS central=https://repo1.maven.org/maven2,apache-snapshot=https://repository.apache.org/content/groups/snapshots/

//DEPS org.apache.camel:camel-bom:${jbang.app.version:4.18.0}@pom
//DEPS org.apache.camel:camel-jbang-core:${jbang.app.version:4.18.0}
//DEPS org.apache.camel.kamelets:camel-kamelets:${jbang.app.version:4.18.0}

package main;

import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;

public class CamelJBang {

    public static void main(String... args) {
        CamelJBangMain.run(args);
    }

}

This does not work, but it should:

  • $JBANG run camel:4.17.0 version

This works (simulating what we expect from alias versioning):

  • $JBANG run -Djbang.app.version=4.17.0 camel version

@maxandersen
Copy link
Copy Markdown
Collaborator Author

Ok that is not apps jvm environment. That's when the script it build into a jar.

You'll need to pass the value via properties as you showed before. The app version is not automatically "inherited"

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 29, 2026

I hope we can change this. We don't necessarily want a separate Java file for each version of an app.

Below is how JBang support for Camel is defined today. How do you propose that this be changed so that

  • jbang app install camel:4.17.0@apache/camel

will work with a release of JBang that support alias versioning?

https://camel.apache.org/manual/camel-jbang.html

{
	"catalogs": {},
	"aliases": {
	  "camel": {
		"script-ref": "dsl/camel-jbang/camel-jbang-main/dist/CamelJBang.java",
		"description": "Run Apache Camel routes easily"
	  }
	}
}
///usr/bin/env jbang "$0" "$@" ; exit $?

//JAVA 21+
//REPOS central=https://repo1.maven.org/maven2,apache-snapshot=https://repository.apache.org/content/groups/snapshots/
//DEPS org.apache.camel:camel-bom:${camel.jbang.version:4.18.0}@pom
//DEPS org.apache.camel:camel-jbang-core:${camel.jbang.version:4.18.0}
//DEPS org.apache.camel.kamelets:camel-kamelets:${camel-kamelets.version:4.18.0}

package main;

import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;

/**
 * Main to run CamelJBang
 */
public class CamelJBang {

    public static void main(String... args) {
        CamelJBangMain.run(args);
    }
}

@maxandersen
Copy link
Copy Markdown
Collaborator Author

This is the chicken-egg thing I talked about - the alias itself is not versioned its the "thing" the alias points to that needs to be versioned; and when you point to a file relatively in a main branch...you are implicitly saying "give me whatever is the latest".

So you need to pick it you want whatever is latest or a specific named version.

So your alias could point to: camel@apache/camel/camel-4.18.1 and use the versioned name of the jbang catalog
or use org.apache.camel:camel-jbang-core:4.18.0 + extra reps and use the versioned maven GAV you are after.

Do note that in the first one the version number is actually "camel-4.18.1" which might not be what you are after but that is the version name used.

You can do camel@apache/camel/${jbang.app.version:main} but then you'll need to use camel:camel-4.18.1@apache/camel

But now you are having the main repo define an alias in the same place as what this alias points to - it is not that clean and im not seeing how/what we can change without making it harder for everything else.

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 29, 2026

the alias itself is not versioned its the "thing" the alias points to that needs to be versioned

Agreed.

The I don't understand why we don't view CamelJbang.java file as a "template" file to be used to create a version specific file to be compiled by javac. The alias script-ref points to "template" file CamelJBang.java which is parameterized with property jbang.app.version. Just update JBang set set property "jbang.app.version" in the JBang JVM (not the App JVM) and then the property will be available when "template" file CamelJBang.java is processed by JBang.

The locking mechanism that you submitted in another PR I guess will be needed to ensure that apps installed in this way are not updated.

@maxandersen
Copy link
Copy Markdown
Collaborator Author

It already is treated as a "template file" - it's done when you build.

The thing is that I don't see a consistent way we can be ok applying version to script files without having a version context.

That might be we need to such thing as default version but given i already showed you multiple ways to do it without it could you tell me why those are not applicable/sufficient ?

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 30, 2026

My concern is that versioning Camel/JBang is a bit cumbersome, but it works.

To support versions 4.17.0, 4.18.0 and 4.18.1, I need to create three distinct CamelJBang.java files:

image

They are all the same except for the hard-coded version number they each contain.

camel/4.17.0/CamelJBang.java

....

//DEPS org.apache.camel:camel-bom:4.17.0@pom
//DEPS org.apache.camel:camel-jbang-core:4.17.0
//DEPS org.apache.camel.kamelets:camel-kamelets:4.17.0

....

camel/4.18.0/CamelJBang.java

....

//DEPS org.apache.camel:camel-bom:4.18.0@pom
//DEPS org.apache.camel:camel-jbang-core:4.18.0
//DEPS org.apache.camel.kamelets:camel-kamelets:4.18.0

....

camel/4.18.1/CamelJBang.java

....

//DEPS org.apache.camel:camel-bom:4.18.1@pom
//DEPS org.apache.camel:camel-jbang-core:4.18.1
//DEPS org.apache.camel.kamelets:camel-kamelets:4.18.0

....

jbang-catalog.json

    "camel": {
      "script-ref": "./camel/${jbang.app.version:4.18.1}/CamelJBang.java"
    }

The 4.18.1 CamelJBang.java example shows that one version number is not always sufficient to meet alll //DEPS requirements. In this case camel-kamelets did not have a 4.18.1 version, and had to remain at 4.18.0 although the other camel JARs do use version 4.18.1. This is a convincing counter example, that shows that versioning as it is currently implemented provide the flexibility required to address issues like this.

But it makes me wonder if one should not provide "hints" to the user of what "versions" are available. To that end a "versions" property that can be queried would be really useful.

    "camel": {
      "script-ref": "./camel/${jbang.app.version:4.18.1}/CamelJBang.java",
      "versions": {
        "4.18.1": "Recommended stable version",
        "4.17.0": "Previous stable version",
        "4.9.0": "Legacy version"
      }
    }

My concern is that versioning Camel/JBang is a bit cumbersome, but it works.

Getting back to my opening sentence. No concerns anymore.

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 30, 2026

The only remaining issue I'm aware of is fixing:

  • [jbang] [ERROR] Unknown catalog 'apps/camel/${jbang.app.version:4.16.0}/jbang-catalog.json'

And then, optionally, adding a "versions" property. :-)

@maxandersen
Copy link
Copy Markdown
Collaborator Author

To support versions 4.17.0, 4.18.0 and 4.18.1, I need to create three distinct CamelJBang.java files:

you write as you create these in the same branch - but you don't have to; you already have that file in multiple versions in the tagged versions at https://github.com/apache/camel/commits/main/dsl/camel-jbang/camel-jbang-main/dist/CamelJBang.java

If anything I would just make an alias and skip cameljbang.java completely. It is not doing anything besides calling a main method.

@maxandersen
Copy link
Copy Markdown
Collaborator Author

  "camel": {
      "script-ref": "./camel/${jbang.app.version:4.18.1}/CamelJBang.java",
      "versions": {
        "4.18.1": "Recommended stable version",
        "4.17.0": "Previous stable version",
        "4.9.0": "Legacy version"
      }
    }

I'm not a superfan, but I can see how it could be useful for querying/documentation. Open separate issue and lets see.

To support versions 4.17.0, 4.18.0 and 4.18.1, I need to create three distinct CamelJBang.java files:

No, I don't see why you need to do that. Just use property replacements or different aliases. Your own suggested ways for it required these too.

But again, as I said multiple times you are trying to do something I consider extremely niche of enabling multiple version support for something (i.e. CamelJBang) by putting a source file inside the same source repo of the thing you try to version. That is always going to be messy.

@maxandersen
Copy link
Copy Markdown
Collaborator Author

  • [jbang] [ERROR] Unknown catalog 'apps/camel/${jbang.app.version:4.16.0}/jbang-catalog.json'

what specifcially generated that error?

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Mar 30, 2026

what specifcially generated that error?

    "hw": {
      "script-ref": "hello@./apps/hello/1.0.0/jbang-catalog.json"
    },
    "hw2": {
      "script-ref": "hello@./apps/hello/${jbang.app.version:1.0.0}/jbang-catalog.json"
    }

Works

  • jbang run hw

Fails

  • jbang run hw
    [jbang] [ERROR] Unknown catalog './apps/hello/${jbang.app.version:1.0.0}/jbang-catalog.json'

@wfouche
Copy link
Copy Markdown
Contributor

wfouche commented Apr 6, 2026

[jbang] [ERROR] Unknown catalog './apps/hello/${jbang.app.version:1.0.0}/jbang-catalog.json'

@maxandersen , this is the only issue that needs to be resolved before this feature can be released. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants