Skip to content
Merged
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
46 changes: 42 additions & 4 deletions osate-cli/OSATE-CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,62 @@ osate-cli help
osate-cli -h | --help
```

Prints usage. The long form (`help`) prints the version banner, followed by the full
description of every command and exit codes; the short form (`-h`/`--help`) prints just
the synopsis lines.
Prints usage. The long form (`help`) prints the version banner and the build it came
from, followed by the full description of every command and exit codes; the short form
(`-h`/`--help`) prints just the synopsis lines.

```text
osate-cli 0.1.0
language server 0.1.0.v20260902-1320 (7fbfec9)
OSATE 2.19.0.vfinal (4256148)
```

### version

```
osate-cli -v | --version
```

Prints `osate-cli <version>` and exits 0.
Prints `osate-cli <version>` and exits 0. Exactly one line, with nothing after the
version, so it stays parseable; use `help` for the build provenance.

The version is declared in exactly one place: the `<revision>` property of
`osate-cli/pom.xml`. Maven filters it into `org/osate/cli/version.properties` inside
`osate-cli.jar`, and the packaging scripts read it back out of that jar to version the
release tarballs, `.deb`, `.rpm`, and Homebrew formula. The version reported here is
therefore always the version of the package the CLI was installed from.

### Build provenance

`help` also reports the bundled language server and the OSATE it was built against,
because the CLI's own version says nothing about either, and most behaviour comes from
them:

| Key in `version.properties` | Meaning |
| --- | --- |
| `ls.version` | Bundle version of the bundled language server, e.g. `0.1.0.v20260902-1320` |
| `ls.commit` | Commit of the `osate/aadl-tooling` repository it was built from |
| `osate.version` | OSATE version, e.g. `2.19.0.vfinal` |
| `osate.commit` | The reviewed `osate2` gitlink it was built against |

None of these can be discovered at runtime. OSATE bundles carry independent versions
(`org.osate.aadl2` is 6.1.1, not 2.19.0) and no bundle manifest records a commit, so
`scripts/build-test-release` supplies all four and Maven filters them into the jar
alongside the version.

A build that bypasses that script — a bare `mvn -f osate-cli/pom.xml verify` — reports
`unknown` for all four. That is deliberate, so a hand-built CLI does not claim a
provenance it does not have; `build-release-artifacts.sh` refuses to package a
distribution that reports `unknown`, and also refuses one whose `osate.commit` disagrees
with the current gitlink.

`ls.commit` gains a `-dirty` suffix when the working tree had uncommitted changes, since
the commit alone would otherwise appear to identify code that was not what got compiled.
Packaging warns about it rather than failing, so local packaging tests still work.

Commits are abbreviated to seven characters in `help`; `version.properties` keeps them in
full for anything parsing the jar.

### project

The local project commands treat the current working directory as a workspace. Projects
Expand Down
4 changes: 4 additions & 0 deletions osate-cli/osate-cli/manual-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ process in its own process tree.
| 1.4a | `osate-cli -v` | exit 0; `osate-cli <version>` on stdout |
| 1.4b | `osate-cli --version` | exit 0; same output as 1.4a |
| 1.4c | `osate-cli -v` from an installed package | version matches the installed Homebrew/deb/rpm package version (`brew list --versions osate-cli`, `dpkg -s osate-cli`, or `rpm -q osate-cli`) |
| 1.4d | `osate-cli help` | lines 2-3 report the build: ` language server <version> (<commit>)` and ` OSATE <version> (<commit>)`, both indented |
| 1.4e | `osate-cli --version` | still exactly one line; provenance appears only in `help` |
| 1.4f | `osate-cli help` from an installed package | neither provenance line says `unknown`; a released package always records what it was built from |
| 1.4g | `osate-cli help` from a locally built dist with a dirty tree | the language-server commit carries a `-dirty` suffix |
| 1.5 | `osate-cli c1 bogus` | exit 2; "missing -p <port>" or unknown-command error |
| 1.6 | `osate-cli c1 -p 1 bogus` | exit 2; "unknown command: bogus" |
| 1.7 | `osate-cli c1 -p abc ping` | exit 2; "-p/--port must be an integer in 1..65535: abc" |
Expand Down
26 changes: 24 additions & 2 deletions osate-cli/osate-cli/src/main/java/org/osate/cli/ArgParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -364,13 +364,35 @@ private static String projectUsage() {
return "usage: project <list|create|show|add-dependency|remove-dependency|validate> [args...]";
}

/** Version banner printed by {@code -v}/{@code --version} and atop the help text. */
/**
* Version banner printed by {@code -v}/{@code --version} and atop the help text.
*
* <p>Deliberately a single line with nothing after the version: the assembled-CLI
* integration test compares {@code --version} output for exact equality, and the
* packaging scripts' {@code --expect-version} check parses it. Build provenance goes
* in {@link #versionDetail()}, which only {@link #help()} prints.
*/
public static String versionLine() {
return "osate-cli " + Version.get();
}

/**
* Indented provenance lines describing what this CLI was built from.
*
* <p>Answers "which OSATE is this?", which the version alone does not: the behaviour
* a user sees comes mostly from the bundled language server and the OSATE underneath
* it. Commits are abbreviated for reading; {@code version.properties} inside the jar
* keeps them in full.
*/
public static String versionDetail() {
return " language server " + Version.languageServerVersion() + " ("
+ Version.abbreviate(Version.languageServerCommit(), 7) + ")\n"
+ " OSATE " + Version.osateVersion() + " ("
+ Version.abbreviate(Version.osateCommit(), 7) + ")";
}

public static String help() {
return versionLine() + "\n\n" + usage() + "\n" + """
return versionLine() + "\n" + versionDetail() + "\n\n" + usage() + "\n" + """

osate-cli is a command-line client for the OSATE AADL language server. Remote
commands talk to a long-lived workspace server (one per workspace) over a TCP
Expand Down
89 changes: 78 additions & 11 deletions osate-cli/osate-cli/src/main/java/org/osate/cli/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import java.util.Properties;

/**
* Reports the osate-cli release version.
* Reports the osate-cli release version and what it was built against.
*
* <p>The version is declared once, in the {@code <revision>} property of
* {@code osate-cli/pom.xml}. Maven resource filtering writes it into
Expand All @@ -36,6 +36,12 @@
* rpm, and Homebrew packages. The version reported here therefore always matches the
* package the CLI was installed from.
*
* <p>The same resource carries the bundled language server's version and commit and the
* OSATE version and commit. None of those can be discovered at runtime: OSATE bundles
* carry independent versions ({@code org.osate.aadl2} is 6.1.1, not 2.19.0) and no
* bundle manifest records a commit, so {@code scripts/build-test-release} bakes them in.
* A build that bypasses that script reports {@link #UNKNOWN_PROVENANCE} for them.
*
* <p>A properties resource is used rather than the jar manifest's
* {@code Implementation-Version} because the resource is also present when running from
* {@code target/classes} (tests and IDE launches), and because manifest values are line
Expand All @@ -46,9 +52,18 @@ public final class Version {
/** Reported when the filtered resource is missing, e.g. from a hand-assembled classpath. */
public static final String UNKNOWN = "0.0.0-dev";

/** Reported for build provenance that the build did not supply. */
public static final String UNKNOWN_PROVENANCE = "unknown";

private static final String RESOURCE = "/org/osate/cli/version.properties";

private static final String VERSION = load();
private static final Properties PROPS = load();

private static final String VERSION = read("version", UNKNOWN);
private static final String LS_VERSION = read("ls.version", UNKNOWN_PROVENANCE);
private static final String LS_COMMIT = read("ls.commit", UNKNOWN_PROVENANCE);
private static final String OSATE_VERSION = read("osate.version", UNKNOWN_PROVENANCE);
private static final String OSATE_COMMIT = read("osate.commit", UNKNOWN_PROVENANCE);

private Version() {
}
Expand All @@ -58,18 +73,70 @@ public static String get() {
return VERSION;
}

private static String load() {
/** Bundle version of the language server this CLI ships, e.g. {@code 0.1.0.v20260902-1313}. */
public static String languageServerVersion() {
return LS_VERSION;
}

/** Commit of this repository that the bundled language server was built from. */
public static String languageServerCommit() {
return LS_COMMIT;
}

/** OSATE version the language server was built against, e.g. {@code 2.19.0.vfinal}. */
public static String osateVersion() {
return OSATE_VERSION;
}

/** The reviewed {@code osate2} gitlink the language server was built against. */
public static String osateCommit() {
return OSATE_COMMIT;
}

/** Whether every provenance value was supplied by the build. */
public static boolean hasCompleteProvenance() {
return !UNKNOWN.equals(VERSION) && !UNKNOWN_PROVENANCE.equals(LS_VERSION)
&& !UNKNOWN_PROVENANCE.equals(LS_COMMIT) && !UNKNOWN_PROVENANCE.equals(OSATE_VERSION)
&& !UNKNOWN_PROVENANCE.equals(OSATE_COMMIT);
}

/**
* Shortens a commit for display, keeping any trailing marker.
*
* <p>Only the leading hexadecimal run is shortened, so {@code <sha>-dirty} becomes
* {@code 7fbfec9-dirty} rather than losing the suffix that says the value cannot be
* trusted, and a non-commit placeholder like {@code unknown} is returned untouched
* instead of being truncated into something that looks like a commit.
*/
public static String abbreviate(String commit, int length) {
if (commit == null) {
return null;
}
var hex = 0;
while (hex < commit.length() && Character.digit(commit.charAt(hex), 16) >= 0) {
hex++;
}
if (hex <= length) {
return commit;
}
return commit.substring(0, length) + commit.substring(hex);
}

private static Properties load() {
var props = new Properties();
try (InputStream in = Version.class.getResourceAsStream(RESOURCE)) {
if (in == null) {
return UNKNOWN;
if (in != null) {
props.load(in);
}
var props = new Properties();
props.load(in);
var version = props.getProperty("version", "").trim();
// An unfiltered resource still holds the literal Maven expression.
return version.isEmpty() || version.startsWith("${") ? UNKNOWN : version;
} catch (IOException e) {
return UNKNOWN;
// Fall through to the empty properties; every value then reports its fallback.
}
return props;
}

private static String read(String key, String fallback) {
var value = PROPS.getProperty(key, "").trim();
// An unfiltered resource still holds the literal Maven expression.
return value.isEmpty() || value.startsWith("${") ? fallback : value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@
# Generated from the <revision> property in osate-cli/pom.xml by Maven resource
# filtering. Do not edit the version by hand; bump <revision> instead.
version=${project.version}

# What this CLI was built against. Neither of the OSATE values can be recovered at
# runtime: OSATE bundles carry independent versions (org.osate.aadl2 is 6.1.1, not
# 2.19.0) and no bundle manifest records a commit, so they have to be baked in
# here. scripts/build-test-release supplies them; a build that does not go through
# it leaves them 'unknown'.
ls.version=${aadl.ls.version}
ls.commit=${aadl.ls.commit}
osate.version=${osate.version}
osate.commit=${osate.commit}
32 changes: 32 additions & 0 deletions osate-cli/osate-cli/src/test/java/org/osate/cli/ArgParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,30 @@ void versionLineReportsPackagedVersion() {
assertEquals("osate-cli " + Version.get(), ArgParser.versionLine());
}

/**
* {@code --version} prints this and nothing else, so it must stay one line. The
* assembled-CLI test compares that output for exact equality and the packaging
* scripts parse it, so appending build provenance here would break both. Provenance
* belongs in {@code versionDetail()}, which only {@code help()} prints.
*/
@Test
void versionLineStaysASingleLine() {
assertEquals(1, ArgParser.versionLine().lines().count());
}

@Test
void versionDetailReportsWhatTheCliWasBuiltFrom() {
var detail = ArgParser.versionDetail();

assertTrue(detail.contains("language server"), () -> detail);
assertTrue(detail.contains("OSATE"), () -> detail);
assertTrue(detail.contains(Version.languageServerVersion()), () -> detail);
assertTrue(detail.contains(Version.osateVersion()), () -> detail);
assertTrue(detail.contains(Version.abbreviate(Version.osateCommit(), 7)), () -> detail);
// Every line indented, so the banner stays the only flush-left line in help.
assertTrue(detail.lines().allMatch(line -> line.startsWith(" ")), () -> detail);
}

@Test
void helpStartsWithVersionBanner() {
var help = ArgParser.help();
Expand All @@ -114,6 +138,14 @@ void helpStartsWithVersionBanner() {
assertTrue(ArgParser.usage().contains("osate-cli -v | --version"));
}

@Test
void helpIncludesBuildProvenanceUnderTheBanner() {
var lines = ArgParser.help().lines().toList();

assertEquals(ArgParser.versionLine(), lines.get(0));
assertEquals(ArgParser.versionDetail(), String.join("\n", lines.subList(1, 3)));
}

@Test
void helpMatchesLocalAndLanguageServerDispatch() {
var help = ArgParser.help();
Expand Down
49 changes: 49 additions & 0 deletions osate-cli/osate-cli/src/test/java/org/osate/cli/VersionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
package org.osate.cli;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.Test;
Expand All @@ -48,4 +51,50 @@ void matchesVersionDeclaredInPom() {
void isResolvedNotFallback() {
assertNotEquals(Version.UNKNOWN, Version.get());
}

/**
* The provenance keys must resolve to something, even in a standalone reactor build
* that supplies no values. A literal {@code ${...}} here would mean the keys were
* added to the resource but not declared as pom properties, which would otherwise
* surface as raw Maven expressions printed in {@code osate-cli help}.
*/
@Test
void provenanceIsNeverAnUnfilteredExpression() {
for (var value : new String[] { Version.languageServerVersion(), Version.languageServerCommit(),
Version.osateVersion(), Version.osateCommit() }) {
assertNotNull(value);
assertFalse(value.isBlank(), "provenance value is blank");
assertFalse(value.startsWith("${"), () -> "unfiltered provenance value: " + value);
}
}

/**
* A standalone {@code mvn -f osate-cli/pom.xml verify} supplies no provenance, so the
* pom defaults must say so rather than report a real-looking value. The release path
* asserts the opposite; see {@code packaging/scripts/build-release-artifacts.sh}.
*/
@Test
void provenanceReportsUnknownWhenTheBuildSuppliesNothing() {
assumeTrue(Version.UNKNOWN_PROVENANCE.equals(Version.osateCommit()),
"this build supplied provenance, so there is nothing to check here");
assertFalse(Version.hasCompleteProvenance());
}

@Test
void abbreviateShortensTheCommitAndKeepsAnyMarker() {
assertEquals("4256148", Version.abbreviate("425614884eaf14312141fbdd3a393ba54ff34b23", 7));

// The -dirty marker says the commit does not describe what was built, so it has
// to survive; dropping it would turn an untrustworthy value into a trustworthy
// looking one. Local builds are the common case here.
assertEquals("7fbfec9-dirty",
Version.abbreviate("7fbfec98e4f2d78fff9ebf8dca7c866cbad8029b-dirty", 7));

// Not a commit at all: returning it untouched keeps it obviously not a commit.
assertEquals("unknown", Version.abbreviate("unknown", 7));

// Already short enough, even though every character happens to be hex.
assertEquals("abc", Version.abbreviate("abc", 7));
assertNull(Version.abbreviate(null, 7));
}
}
10 changes: 10 additions & 0 deletions osate-cli/packaging/scripts/build-release-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,17 @@ OSATE_CLI_VERSION=$(version_from_dist "$dist_dir")
if [ -n "$expect_version" ] && [ "$expect_version" != "$OSATE_CLI_VERSION" ]; then
die "dist reports version $OSATE_CLI_VERSION, but --expect-version is $expect_version"
fi

# Refuse to package a CLI that cannot say which OSATE it came from. Compared against
# the gitlink rather than the submodule's HEAD: the gitlink is the reviewed pin, and
# it is readable here even when the submodule is not checked out.
osate_gitlink=$(git -C "$repo_root" ls-files --stage osate2 2>/dev/null |
awk '$1 == "160000" { print $2 }')
require_release_provenance "$dist_dir" "$osate_gitlink"

echo "Packaging $OSATE_CLI_PACKAGE_NAME $OSATE_CLI_VERSION (from $dist_dir/osate-cli.jar)"
echo " language server $(provenance_from_dist "$dist_dir" ls.version) ($(provenance_from_dist "$dist_dir" ls.commit))"
echo " OSATE $(provenance_from_dist "$dist_dir" osate.version) ($(provenance_from_dist "$dist_dir" osate.commit))"

downloads_dir="$output_dir/downloads"
staging_dir="$output_dir/staging"
Expand Down
Loading
Loading