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
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,9 @@ jobs:
NPM_VERSION="$(python client/python/scripts/check_version.py --node)"
JAVA_VERSION="$(python client/python/scripts/check_version.py --java)"
RUST_VERSION="$(python client/python/scripts/check_version.py --rust)"
# Nothing publishes the editors, but their manifests carry the same
# version, so a tag fails here when one disagrees; no export needed.
python client/python/scripts/check_version.py --editors > /dev/null
echo "export OPENSYSML_VERSION=${VERSION}" >> "$BASH_ENV"
echo "export NPM_VERSION=${NPM_VERSION}" >> "$BASH_ENV"
echo "export JAVA_VERSION=${JAVA_VERSION}" >> "$BASH_ENV"
Expand Down
6 changes: 6 additions & 0 deletions changes/unreleased/editor-versions-lockstep.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **The editors carry the core version, in lockstep with the clients.** The VS
Code and SysON frontend package.json files (and their locks), the Cameo and
SysON poms and their children's `<parent><version>` moved from `0.1.0` /
`0.1.0-SNAPSHOT` to `0.9.0`, the version `_version.py` declares. Nothing
publishes the editors, but `check_version.py --editors` now fails a release
early when any of them disagrees, alongside a pytest gate on every PR.
130 changes: 129 additions & 1 deletion client/python/scripts/check_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
With `--node`, `--java` or `--rust` the version client/node/package.json,
client/java/pom.xml or client/rust/opensysml/Cargo.toml declares is checked
the same way and printed instead — all three clients are published from the
same tag, at the SemVer spelling of the same version.
same tag, at the SemVer spelling of the same version. `--editors` runs the
same check over every manifest an editor takes its version from: nothing
publishes the editors, but their versions follow the core's, so a release
still fails early when one disagrees.

The core tags are SemVer and the package version is PEP 440, so the tag is
translated before the comparison: `v0.9.0-rc1` names `0.9.0rc1`. Only the SemVer
Expand Down Expand Up @@ -55,6 +58,24 @@
JAVA_POM = os.path.join(REPO_ROOT, "client", "java", "pom.xml")
RUST_CARGO = os.path.join(REPO_ROOT, "client", "rust", "opensysml", "Cargo.toml")

# Every manifest an editor takes its version from, as (repo-relative path,
# reader kind). The editors carry the core version even though nothing
# publishes them.
EDITOR_MANIFESTS = (
("editors/vscode/package.json", "json"),
("editors/vscode/package-lock.json", "lock"),
("editors/syson/frontend/package.json", "json"),
("editors/syson/frontend/package-lock.json", "lock"),
("editors/cameo/pom.xml", "pom"),
("editors/syson/pom.xml", "pom"),
("editors/cameo/plugin/pom.xml", "parent"),
("editors/cameo/tools/pom.xml", "parent"),
("editors/cameo/openapi-stubs/pom.xml", "parent"),
("editors/cameo/dist/pom.xml", "parent"),
("editors/syson/backend/pom.xml", "parent"),
("editors/syson/syson-api-stubs/pom.xml", "parent"),
)


class VersionError(Exception):
"""A tag that does not name a publishable opensysml version."""
Expand Down Expand Up @@ -332,6 +353,106 @@ def rust_version(declared=None, rust=None, tag=None):
)


def lock_declared_version(lock_path):
"""The version a package-lock.json's root package declares.

The lock repeats the package.json version twice — the top-level `version`
and `packages[""]["version"]` — and the two must agree.

Args:
lock_path (str): Path to the package-lock.json

Returns:
str: The declared version

Raises:
VersionError: If either copy is missing or they disagree
"""
with open(lock_path, encoding="utf-8") as f:
lock = json.load(f)
top = lock.get("version")
root = lock.get("packages", {}).get("", {}).get("version")
if not isinstance(top, str) or not isinstance(root, str):
raise VersionError(
f"{lock_path} declares no version: expected both the top-level "
"'version' and packages[\"\"][\"version\"]"
)
if top != root:
raise VersionError(
f"{lock_path} declares {top!r} at the top level but "
f"packages[\"\"] declares {root!r}; the two must agree. Run "
"`npm install --package-lock-only` in the package's directory."
)
return top


def pom_parent_version(pom):
"""The version a child pom's <parent> element names.

Args:
pom (str): Path to the child pom.xml

Returns:
str: The parent version

Raises:
VersionError: If the pom has no <parent><version>
"""
project = ET.parse(pom).getroot()
version = project.findtext(
"{http://maven.apache.org/POM/4.0.0}parent/"
"{http://maven.apache.org/POM/4.0.0}version"
)
if version is None:
raise VersionError(f"{pom} declares no <parent><version>")
return version


_EDITOR_READERS = {
"json": node_declared_version,
"lock": lock_declared_version,
"pom": java_declared_version,
"parent": pom_parent_version,
}


def editors_version(declared=None, tag=None, manifests=EDITOR_MANIFESTS, root=REPO_ROOT):
"""The editors' version, checked over every manifest against _version.py and the tag.

Args:
declared (str, optional): Version opensysml/_version.py declares; read
when omitted
tag (str, optional): Core release tag the release runs from
manifests (tuple): (repo-relative path, reader kind) pairs; defaults to
EDITOR_MANIFESTS
root (str): Repo root the relative paths resolve against

Returns:
str: The version every editor manifest declares

Raises:
VersionError: If any manifest disagrees with _version.py, or the tag
does not spell the version a manifest declares
"""
declared = declared_version() if declared is None else declared
version = None
for relpath, kind in manifests:
version = _client_version(
relpath,
relpath,
"the editors' release",
"The editors",
_EDITOR_READERS[kind](os.path.join(root, relpath)),
declared,
tag,
"set every editor manifest docs/project/releasing.md lists to the "
"SemVer spelling of that version and run `npm install "
"--package-lock-only` in editors/vscode and "
"editors/syson/frontend.",
)
return version


def _client_version(what, file, registry, client_name, client, declared, tag, remedy):
"""The lockstep check the published client manifests share.

Expand Down Expand Up @@ -427,6 +548,11 @@ def main(argv=None):
action="store_true",
help="check and print client/rust/opensysml/Cargo.toml's version instead",
)
clients.add_argument(
"--editors",
action="store_true",
help="check every editor manifest's version and print it instead",
)
parser.add_argument(
"--pre-release",
action="store_true",
Expand All @@ -442,6 +568,8 @@ def main(argv=None):
version = java_version(declared=version, tag=args.tag)
elif args.rust:
version = rust_version(declared=version, tag=args.tag)
elif args.editors:
version = editors_version(declared=version, tag=args.tag)
pre_release = is_pre_release(version)
except VersionError as e:
print(f"error: {e}", file=sys.stderr)
Expand Down
116 changes: 116 additions & 0 deletions client/python/tests/test_check_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,119 @@ def test_main_prints_the_crates_version_the_tag_names(capsys):
def test_main_refuses_rust_and_java_together():
with pytest.raises(SystemExit):
check_version.main(["--tag", "v0.9.0", "--rust", "--java"])


_EDITOR_PARENTS = (
"editors/cameo/plugin/pom.xml",
"editors/cameo/tools/pom.xml",
"editors/cameo/openapi-stubs/pom.xml",
"editors/cameo/dist/pom.xml",
"editors/syson/backend/pom.xml",
"editors/syson/syson-api-stubs/pom.xml",
)


def _editor_tree(root, version):
"""The minimal editor manifest tree editors_version reads, at one version."""
ns = 'xmlns="http://maven.apache.org/POM/4.0.0"'
for pkg in ("editors/vscode", "editors/syson/frontend"):
(root / pkg).mkdir(parents=True, exist_ok=True)
(root / pkg / "package.json").write_text(
f'{{"version": "{version}"}}', encoding="utf-8"
)
(root / pkg / "package-lock.json").write_text(
f'{{"version": "{version}", '
f'"packages": {{"": {{"version": "{version}"}}}}}}',
encoding="utf-8",
)
for relpath in ("editors/cameo/pom.xml", "editors/syson/pom.xml"):
(root / relpath).parent.mkdir(parents=True, exist_ok=True)
(root / relpath).write_text(
f'<project {ns}><version>{version}</version></project>',
encoding="utf-8",
)
for relpath in _EDITOR_PARENTS:
(root / relpath).parent.mkdir(parents=True, exist_ok=True)
(root / relpath).write_text(
f'<project {ns}><parent><version>{version}</version></parent></project>',
encoding="utf-8",
)


def test_editors_version_agrees_with_the_real_tree():
"""Every editor manifest must carry _version.py's SemVer spelling on every commit."""
declared = check_version.declared_version()
version = check_version.editors_version()
assert check_version.pep440_from_semver(version, "v") == declared


def test_main_prints_the_editors_version_the_tag_names(capsys):
version = check_version.editors_version()
assert check_version.main(["--tag", f"v{version}", "--editors"]) == 0
assert capsys.readouterr().out.strip() == version


def test_editors_version_accepts_an_agreeing_tree(tmp_path):
_editor_tree(tmp_path, "0.9.0")
assert (
check_version.editors_version(
declared="0.9.0", tag="v0.9.0", root=str(tmp_path)
)
== "0.9.0"
)


def test_editors_version_rejects_one_disagreeing_json(tmp_path):
_editor_tree(tmp_path, "0.9.0")
(tmp_path / "editors/vscode/package.json").write_text(
'{"version": "0.9.1"}', encoding="utf-8"
)
with pytest.raises(
check_version.VersionError, match="editors/vscode/package.json declares"
):
check_version.editors_version(declared="0.9.0", root=str(tmp_path))


def test_lock_declared_version_rejects_mismatched_copies(tmp_path):
lock = tmp_path / "package-lock.json"
lock.write_text(
'{"version": "0.9.0", "packages": {"": {"version": "0.9.1"}}}',
encoding="utf-8",
)
with pytest.raises(check_version.VersionError, match="must agree"):
check_version.lock_declared_version(str(lock))


def test_pom_parent_version_rejects_a_pom_without_parent_version(tmp_path):
pom = tmp_path / "pom.xml"
pom.write_text(
'<project xmlns="http://maven.apache.org/POM/4.0.0"><parent/></project>',
encoding="utf-8",
)
with pytest.raises(
check_version.VersionError, match="declares no <parent><version>"
):
check_version.pom_parent_version(str(pom))


def test_editors_version_accepts_a_matching_pre_release(tmp_path):
_editor_tree(tmp_path, "0.9.1-rc.1")
assert (
check_version.editors_version(
declared="0.9.1rc1", tag="v0.9.1-rc.1", root=str(tmp_path)
)
== "0.9.1-rc.1"
)


def test_editors_version_rejects_a_tag_that_misspells_the_version(tmp_path):
_editor_tree(tmp_path, "0.9.0")
with pytest.raises(check_version.VersionError, match="must spell it exactly"):
check_version.editors_version(
declared="0.9.0", tag="v0.9.1", root=str(tmp_path)
)


def test_main_refuses_editors_and_node_together():
with pytest.raises(SystemExit):
check_version.main(["--tag", "v0.9.0", "--editors", "--node"])
4 changes: 2 additions & 2 deletions docs/project/nightly.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ And one a stable release does not ship:
from the same commit (as `make vscode-package` does). The extension is side-loaded rather than
published to a marketplace, so the snapshot is where a build of it is picked up. Its
version is the extension manifest's with the snapshot version appended as the pre-release
part — `0.1.0-nightly-<yyyymmdd>-<commit>` — so VS Code tells one night's build from the
part — `0.9.0-nightly-<yyyymmdd>-<commit>` — so VS Code tells one night's build from the
next, installs a later night over an earlier one without `--force`, and ranks any stable
`0.1.0` above them all. It is in `SHA256SUMS.txt` with the rest.
`0.9.0` above them all. It is in `SHA256SUMS.txt` with the rest.

Not in a snapshot: the Windows installer, the Authenticode-signed Windows binaries, the
Homebrew formula, and the PyPI, npm, Maven and crates.io client packages. Those belong to the
Expand Down
19 changes: 17 additions & 2 deletions docs/project/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,16 @@ branch that moves the integration state onto `main`:
spelling as package.json. `client/rust/opensysml/Cargo.toml` follows too:
set `[package] version` to the same spelling and run
`cargo update -p opensysml` in `client/rust` so the lockfile
agrees. Anything else the release
agrees. The editors carry the same spelling too, though nothing publishes them:
`"version"` in `editors/vscode/package.json` and
`editors/syson/frontend/package.json`, each lock regenerated with
`npm install --package-lock-only` in that directory; `<version>` in
`editors/cameo/pom.xml` and `editors/syson/pom.xml`; and `<parent><version>`
in their child poms (`editors/cameo/plugin`, `editors/cameo/tools`,
`editors/cameo/openapi-stubs`, `editors/cameo/dist`, `editors/syson/backend`
and `editors/syson/syson-api-stubs`). `check_version.py --editors` in
`build-python-package` fails the release early when any of them disagrees.
Anything else the release
needs (a doc that names the version) lands here too; a feature does not. Check the wire compatibility
against the released schema, not the branch's own source:
`make proto-breaking BUF_BREAKING_REF=origin/main` (the default baseline is
Expand Down Expand Up @@ -167,7 +176,7 @@ Tags are matched by `/^v.*/` in `.circleci/config.yml`. A tag on a commit that
fails the suite fails the release workflow before anything is published, and so
does a tag whose version `client/python/opensysml/_version.py`,
`client/node/package.json`, `client/java/pom.xml` or
`client/rust/opensysml/Cargo.toml` does not declare.
`client/rust/opensysml/Cargo.toml` or an editor manifest does not declare.

## What CircleCI publishes

Expand Down Expand Up @@ -1256,6 +1265,12 @@ disagree, and the pytest gate in `test_check_version.py` — including the test
that every in-repo reference names the pom's version — runs on every PR that
touches either file. The tag must spell the version exactly, `v` aside.

The editors' own versions are in the same lockstep: every manifest
`check_version.py --editors` reads — the VS Code and SysON frontend
package.json files and their locks, the Cameo and SysON parent poms and their
children's `<parent><version>` — carries the SemVer spelling, and the release
fails early when one disagrees.

### Why the core's tag

The client follows the Python and Node clients' choice (see
Expand Down
11 changes: 11 additions & 0 deletions editors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,14 @@ directory here with its own build.
synchronization are designed only; the discovery against release `v2026.9.0`, module layout,
call sequence and phased plan are in
[`docs/internals/design/syson-plugin.md`](../docs/internals/design/syson-plugin.md).

## Versions

The editors carry the core version — `0.9.0` now — the same lockstep the clients
follow, even though nothing publishes them. `check_version.py --editors` checks
every manifest (the two package.json files and their locks, the Cameo and SysON
poms and their children's `<parent><version>`), and a release fails early when
one disagrees. The client references the editors build against —
`opensysml.client.version` in the Cameo pom and the `opensysml-client`
dependency in the SysON backend pom — are pinned to `client/java/pom.xml` the
same way.
2 changes: 1 addition & 1 deletion editors/cameo/dist/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.openmbee</groupId>
<artifactId>opensysml-cameo-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<version>0.9.0</version>
</parent>
<artifactId>opensysml-cameo-dist</artifactId>
<packaging>pom</packaging>
Expand Down
2 changes: 1 addition & 1 deletion editors/cameo/openapi-stubs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.openmbee</groupId>
<artifactId>opensysml-cameo-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<version>0.9.0</version>
</parent>
<artifactId>opensysml-cameo-openapi-stubs</artifactId>
<name>Compile-only Cameo OpenAPI stubs</name>
Expand Down
2 changes: 1 addition & 1 deletion editors/cameo/plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.openmbee</groupId>
<artifactId>opensysml-cameo-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<version>0.9.0</version>
</parent>
<artifactId>opensysml-cameo-plugin</artifactId>
<name>OpenSysML Cameo plugin</name>
Expand Down
Loading
Loading