Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions agentscope-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,77 @@
</dependencies>

<build>
<resources>
<!-- Non-filtered default, excluding the version file so this block never writes the
same target path as the filtered block below. If both blocks copied
META-INF/agentscope/version.properties, the unfiltered copy (which runs first and
rewrites the target with a newer timestamp than the source) would make the
filtered copy be skipped by maven-resources-plugin (default overwrite=false),
silently packaging the literal ${project.version} placeholder. -->
<resource>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Relying on declaration order to make the filtered copy win is fragile here: both resource blocks read the same src/main/resources tree and the same target path, and maven-resources-plugin skips a copy when the destination is not older than the source (default overwrite=false). Since the unfiltered copy runs first and rewrites target/classes/META-INF/agentscope/version.properties with a newer timestamp than the source, the filtered copy can be skipped and the packaged jar ends up containing the literal ${project.version}. Version.resolveVersionFrom then masks that by returning "unknown", so the failure is silent rather than loud. Safer: drop the catch-all unfiltered block's duplicate and exclude the version file from it, e.g. keep one <resource> for src/main/resources with <excludes><exclude>META-INF/agentscope/version.properties</exclude></excludes> plus the filtered resource — then add an assertion (or a maven-enforcer/groovy check) that the packaged resource no longer contains ${.

<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>META-INF/agentscope/version.properties</exclude>
</excludes>
</resource>
<!-- Filter only the version file so ${project.version} is resolved. This block is the
sole writer of the target version.properties. -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>META-INF/agentscope/version.properties</include>
</includes>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>agentscope-test-version.properties</exclude>
</excludes>
</testResource>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
<includes>
<include>agentscope-test-version.properties</include>
</includes>
</testResource>
</testResources>
<plugins>
<!-- Fail the build loudly if the filtered version resource still contains the
${project.version} placeholder (i.e. Maven resource filtering did not apply).
Without this check the packaged jar would silently carry an unfiltered literal,
and Version.resolveVersionFrom would mask it by returning "unknown". -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>verify-version-resource-filtered</id>
<phase>process-classes</phase>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

process-classes runs on every mvn compile, so this execution also fires in builds that never produced target/classes/META-INF/agentscope/version.properties (resources phase skipped, stale/partial target/). Ant's resourcecontains generally reports a missing resource as its own error, which would replace the curated fail message with something less actionable. If you want the diagnostic to survive those cases, gate it first, e.g. fail when <not><available file="${project.build.outputDirectory}/META-INF/agentscope/version.properties"/></not>, or combine the two conditions with <and>. Not blocking - CI and normal builds are covered.

<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<fail message="META-INF/agentscope/version.properties still contains the unfiltered 'dollar-brace' placeholder (the Maven project.version expression was not resolved): Maven resource filtering did not apply. Check the resource filtering configuration in agentscope-core/pom.xml.">
<condition>
<!-- A single '$' is enough to detect the unmapped
${project.version} placeholder: the filtered file is
just 'version=2.0.3-SNAPSHOT' and contains none. -->
<resourcecontains resource="${project.build.outputDirectory}/META-INF/agentscope/version.properties" substring="$" />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good addition - this is the loud failure the previous round asked for. One nit: substring="$" is a blunt instrument, because any future $ in the file (a comment line, a build-metadata entry) would break the build for an unrelated reason. The thing you actually want to reject is the literal ${project.version} placeholder, and since Maven interpolates ${...} inside the POM, matching on project.version gets you that precision without any escaping trick:

<resourcecontains resource="${project.build.outputDirectory}/META-INF/agentscope/version.properties" substring="project.version" />

A correctly filtered file (version=2.0.3-SNAPSHOT) can never contain that token.

</condition>
</fail>
</target>
</configuration>
</execution>
</executions>
</plugin>
<!-- Publish a test-jar so downstream modules (agentscope-harness) can reuse the
shared test fixtures (MockModel, MockToolkit, TestConstants, TestUtils). -->
<plugin>
Expand Down
132 changes: 130 additions & 2 deletions agentscope-core/src/main/java/io/agentscope/core/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,149 @@
*/
package io.agentscope.core;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* AgentScope version and User-Agent information.
*
* <p>Provides a unified User-Agent string for all model requests to identify AgentScope Java
* clients and collect usage statistics.
*
* <p>The version is resolved at class-load time through a three-level fallback chain:
*
* <ol>
* <li>the {@code META-INF/agentscope/version.properties} classpath resource, whose
* {@code ${project.version}} placeholder is resolved by Maven resource filtering;
* <li>the {@code Implementation-Version} manifest entry of the containing jar, which is
* populated from {@code ${project.version}} by the Maven jar plugin;
* <li>the literal {@code "unknown"} as a last resort (e.g. running straight from IDE
* {@code target/classes} with no manifest), accompanied by a one-time warning log.
* </ol>
*/
public final class Version {

/** AgentScope Java version */
public static final String VERSION = "1.0.13-SNAPSHOT";
private static final Logger log = LoggerFactory.getLogger(Version.class);

private static final String VERSION_RESOURCE = "/META-INF/agentscope/version.properties";

/** Sentinel returned when no build-resolved version can be discovered. */
static final String UNKNOWN = "unknown";

/**
* AgentScope Java version.
*
* <p>Injected at build time by Maven resource filtering from {@code ${project.version}};
* see the class Javadoc for the fallback chain.
*/
public static final String VERSION = resolveVersion();

private Version() {
// Utility class - prevent instantiation
}

private static String resolveVersion() {
// 1) Maven-filtered classpath resource (normal Maven builds, tests).
String fromResource;
try (InputStream in = Version.class.getResourceAsStream(VERSION_RESOURCE)) {
fromResource = resolveVersionFromResource(in);
} catch (IOException e) {
log.debug("Failed to read {} from the classpath", VERSION_RESOURCE, e);
fromResource = UNKNOWN;
}
if (!UNKNOWN.equals(fromResource)) {
return fromResource;
}

// 2) jar manifest Implementation-Version (packaged jars whose META-INF/agentscope
// directory may have been dropped, e.g. by shading or custom packaging).
String fromManifest = resolveVersionFromManifest(getImplementationVersion());
if (!UNKNOWN.equals(fromManifest)) {
log.debug(
"Resolved version from jar manifest Implementation-Version: {}", fromManifest);
return fromManifest;
}

// 3) Last resort: make the failure visible instead of silently using a stale value.
log.warn(
"Could not resolve the AgentScope version from {} or the jar manifest "
+ "Implementation-Version; reporting '{}'. Run against a Maven-built "
+ "classpath for accurate versioning.",
VERSION_RESOURCE,
UNKNOWN);
return UNKNOWN;
}

/**
* Load and resolve the version from the classpath resource stream.
*
* <p>Package-private for unit testing.
*
* @param in stream to {@code version.properties}, or {@code null} when the resource is absent
* @return the resolved version, or {@code "unknown"} when the stream is {@code null} or cannot
* be read
*/
static String resolveVersionFromResource(InputStream in) {
if (in == null) {
return UNKNOWN;
}
try (in) {
Properties props = new Properties();
props.load(in);
return resolveVersionFrom(props);
} catch (IOException e) {
log.debug("Failed to read {}", VERSION_RESOURCE, e);
return UNKNOWN;
}
}

/**
* Resolve the version string from a properties object, guarding against missing, blank,
* and unfiltered ({@code ${...}}) values.
*
* <p>Package-private for unit testing.
*
* @param props properties loaded from the version resource
* @return trimmed version, or {@code "unknown"} when absent, blank, or unfiltered
*/
static String resolveVersionFrom(Properties props) {
if (props == null) {
return UNKNOWN;
}
String version = props.getProperty("version");
// Guard against an unfiltered ${project.version} literal, e.g. when running
// from an IDE that does not run Maven resource filtering.
if (version != null && !version.isBlank() && !version.startsWith("${")) {
return version.trim();
}
return UNKNOWN;
}

/**
* Resolve the version from the jar manifest's {@code Implementation-Version} entry.
*
* <p>Package-private for unit testing.
*
* @param implementationVersion manifest value, or {@code null} when absent
* @return the resolved version, or {@code "unknown"} when absent, blank, or unfiltered
*/
static String resolveVersionFromManifest(String implementationVersion) {
if (implementationVersion != null
&& !implementationVersion.isBlank()
&& !implementationVersion.startsWith("${")) {
return implementationVersion.trim();
}
return UNKNOWN;
}

private static String getImplementationVersion() {
Package pkg = Version.class.getPackage();
return pkg == null ? null : pkg.getImplementationVersion();
}

/**
* Generate standard User-Agent string for all models.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version=${project.version}
Loading
Loading