diff --git a/.circleci/config.yml b/.circleci/config.yml index 1b238bc55f..3e804ff794 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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" diff --git a/changes/unreleased/editor-versions-lockstep.changed.md b/changes/unreleased/editor-versions-lockstep.changed.md new file mode 100644 index 0000000000..1a4786b9c6 --- /dev/null +++ b/changes/unreleased/editor-versions-lockstep.changed.md @@ -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 `` 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. diff --git a/client/python/scripts/check_version.py b/client/python/scripts/check_version.py index b9d8ab7315..274dc03647 100644 --- a/client/python/scripts/check_version.py +++ b/client/python/scripts/check_version.py @@ -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 @@ -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.""" @@ -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 element names. + + Args: + pom (str): Path to the child pom.xml + + Returns: + str: The parent version + + Raises: + VersionError: If the pom has no + """ + 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 ") + 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. @@ -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", @@ -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) diff --git a/client/python/tests/test_check_version.py b/client/python/tests/test_check_version.py index 484c54271a..53e3c2097c 100644 --- a/client/python/tests/test_check_version.py +++ b/client/python/tests/test_check_version.py @@ -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'{version}', + encoding="utf-8", + ) + for relpath in _EDITOR_PARENTS: + (root / relpath).parent.mkdir(parents=True, exist_ok=True) + (root / relpath).write_text( + f'{version}', + 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( + '', + encoding="utf-8", + ) + with pytest.raises( + check_version.VersionError, match="declares no " + ): + 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"]) diff --git a/docs/project/nightly.md b/docs/project/nightly.md index f6122d63c2..fd98a260e0 100644 --- a/docs/project/nightly.md +++ b/docs/project/nightly.md @@ -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--` — so VS Code tells one night's build from the + part — `0.9.0-nightly--` — 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 diff --git a/docs/project/releasing.md b/docs/project/releasing.md index c71a04585f..203b631b21 100644 --- a/docs/project/releasing.md +++ b/docs/project/releasing.md @@ -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; `` in + `editors/cameo/pom.xml` and `editors/syson/pom.xml`; and `` + 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 @@ -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 @@ -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 `` — 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 diff --git a/editors/README.md b/editors/README.md index b393879f32..d817fe5cc3 100644 --- a/editors/README.md +++ b/editors/README.md @@ -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 ``), 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. diff --git a/editors/cameo/dist/pom.xml b/editors/cameo/dist/pom.xml index 4e0cbefaea..61f7e07786 100644 --- a/editors/cameo/dist/pom.xml +++ b/editors/cameo/dist/pom.xml @@ -4,7 +4,7 @@ org.openmbee opensysml-cameo-parent - 0.1.0-SNAPSHOT + 0.9.0 opensysml-cameo-dist pom diff --git a/editors/cameo/openapi-stubs/pom.xml b/editors/cameo/openapi-stubs/pom.xml index 11dc26ee18..93eed9c4bb 100644 --- a/editors/cameo/openapi-stubs/pom.xml +++ b/editors/cameo/openapi-stubs/pom.xml @@ -4,7 +4,7 @@ org.openmbee opensysml-cameo-parent - 0.1.0-SNAPSHOT + 0.9.0 opensysml-cameo-openapi-stubs Compile-only Cameo OpenAPI stubs diff --git a/editors/cameo/plugin/pom.xml b/editors/cameo/plugin/pom.xml index 99425cc76b..c98a715e50 100644 --- a/editors/cameo/plugin/pom.xml +++ b/editors/cameo/plugin/pom.xml @@ -4,7 +4,7 @@ org.openmbee opensysml-cameo-parent - 0.1.0-SNAPSHOT + 0.9.0 opensysml-cameo-plugin OpenSysML Cameo plugin diff --git a/editors/cameo/pom.xml b/editors/cameo/pom.xml index eab125553f..fa2cae5623 100644 --- a/editors/cameo/pom.xml +++ b/editors/cameo/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.openmbee opensysml-cameo-parent - 0.1.0-SNAPSHOT + 0.9.0 pom OpenSysML Cameo plugin diff --git a/editors/cameo/tools/pom.xml b/editors/cameo/tools/pom.xml index ef447ed1ea..fed47d64e8 100644 --- a/editors/cameo/tools/pom.xml +++ b/editors/cameo/tools/pom.xml @@ -4,7 +4,7 @@ org.openmbee opensysml-cameo-parent - 0.1.0-SNAPSHOT + 0.9.0 opensysml-cameo-tools OpenSysML Cameo build tools diff --git a/editors/syson/backend/pom.xml b/editors/syson/backend/pom.xml index 5846dead2a..0fbdf3a279 100644 --- a/editors/syson/backend/pom.xml +++ b/editors/syson/backend/pom.xml @@ -7,7 +7,7 @@ org.openmbee opensysml-syson - 0.1.0-SNAPSHOT + 0.9.0 opensysml-syson-backend diff --git a/editors/syson/frontend/package-lock.json b/editors/syson/frontend/package-lock.json index 2ec48424cb..5834ddb75c 100644 --- a/editors/syson/frontend/package-lock.json +++ b/editors/syson/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openmbee/opensysml-syson", - "version": "0.1.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@openmbee/opensysml-syson", - "version": "0.1.0", + "version": "0.9.0", "devDependencies": { "@apollo/client": "3.10.4", "@emotion/react": "11.14.0", diff --git a/editors/syson/frontend/package.json b/editors/syson/frontend/package.json index 40ebac3a40..6656b0ec4b 100644 --- a/editors/syson/frontend/package.json +++ b/editors/syson/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@openmbee/opensysml-syson", - "version": "0.1.0", + "version": "0.9.0", "private": false, "type": "module", "main": "dist/opensysml-syson.cjs.js", diff --git a/editors/syson/pom.xml b/editors/syson/pom.xml index 06b055925e..9dff04f158 100644 --- a/editors/syson/pom.xml +++ b/editors/syson/pom.xml @@ -6,7 +6,7 @@ org.openmbee opensysml-syson - 0.1.0-SNAPSHOT + 0.9.0 pom OpenSysML SysON integration diff --git a/editors/syson/syson-api-stubs/pom.xml b/editors/syson/syson-api-stubs/pom.xml index f9ba211589..89f1f57dc7 100644 --- a/editors/syson/syson-api-stubs/pom.xml +++ b/editors/syson/syson-api-stubs/pom.xml @@ -7,7 +7,7 @@ org.openmbee opensysml-syson - 0.1.0-SNAPSHOT + 0.9.0 opensysml-syson-api-stubs diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index 167b3bde23..b58587959d 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "opensysml-sysml", - "version": "0.1.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opensysml-sysml", - "version": "0.1.0", + "version": "0.9.0", "license": "Apache-2.0", "dependencies": { "elkjs": "0.12.0", diff --git a/editors/vscode/package.json b/editors/vscode/package.json index ed3bd90e86..4a94fefee9 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "opensysml-sysml", "displayName": "SysML v2 (OpenSysML)", "description": "SysML v2 and KerML syntax highlighting and language support backed by the OpenSysML language server.", - "version": "0.1.0", + "version": "0.9.0", "publisher": "open-mbee", "license": "Apache-2.0", "repository": { diff --git a/scripts/ci-changed-areas-test.sh b/scripts/ci-changed-areas-test.sh index d0b3ee22ae..611dbbc4cd 100755 --- a/scripts/ci-changed-areas-test.sh +++ b/scripts/ci-changed-areas-test.sh @@ -55,8 +55,14 @@ case_ rust-manifest python,rust client/rust/opensysml/Cargo.toml case_ rust-lock python,rust client/rust/Cargo.lock case_ julia-only julia client/julia/OpenSysML/src/connection.jl case_ matlab-only matlab client/matlab/+opensysml/call.m -case_ vscode-only vscode editors/vscode/package.json +case_ vscode-manifest python,vscode editors/vscode/package.json +case_ vscode-lock python,vscode editors/vscode/package-lock.json +case_ syson-frontend-manifest python,syson editors/syson/frontend/package.json +case_ syson-frontend-lock python,syson editors/syson/frontend/package-lock.json +case_ syson-manifest python,syson editors/syson/pom.xml case_ cameo-manifest cameo,python editors/cameo/pom.xml +case_ cameo-child-manifest cameo,python editors/cameo/plugin/pom.xml +case_ syson-api-stubs-manifest python,syson editors/syson/syson-api-stubs/pom.xml case_ syson-backend-manifest python,syson editors/syson/backend/pom.xml case_ syson-readme docs,syson editors/syson/README.md # The grammar generator and its committed output are held together by a Go test. diff --git a/scripts/ci-changed-areas.sh b/scripts/ci-changed-areas.sh index 8311919d51..d82d830caa 100755 --- a/scripts/ci-changed-areas.sh +++ b/scripts/ci-changed-areas.sh @@ -53,9 +53,10 @@ emit go "$service" emit docs "$( { [[ "$service" = true ]] || matches "$docs_pattern"; } && echo true || echo false)" emit node "$( { [[ "$service" = true ]] || matches "$node_pattern"; } && echo true || echo false)" # The client-manifest/Python version lockstep tests live in the Python suite, -# so the Node, Java and Rust manifests and the editors' client references have +# so the Node, Java and Rust manifests, every editor manifest +# check_version.py --editors reads, and the editors' client references have # to run it too. -emit python "$( { [[ "$service" = true ]] || matches "$python_pattern" || matches '^client/node/package\.json$' || matches '^client/java/pom\.xml$' || matches '^client/rust/opensysml/Cargo\.toml$' || matches '^client/rust/Cargo\.lock$' || matches '^editors/cameo/pom\.xml$' || matches '^editors/syson/backend/pom\.xml$'; } && echo true || echo false)" +emit python "$( { [[ "$service" = true ]] || matches "$python_pattern" || matches '^client/node/package\.json$' || matches '^client/java/pom\.xml$' || matches '^client/rust/opensysml/Cargo\.toml$' || matches '^client/rust/Cargo\.lock$' || matches '^editors/(cameo|syson(/backend)?)/pom\.xml$' || matches '^editors/(cameo/(plugin|tools|openapi-stubs|dist)|syson/syson-api-stubs)/pom\.xml$' || matches '^editors/(vscode|syson/frontend)/package(-lock)?\.json$'; } && echo true || echo false)" emit java "$( { [[ "$service" = true ]] || matches "$java_pattern"; } && echo true || echo false)" emit rust "$( { [[ "$service" = true ]] || matches "$rust_pattern"; } && echo true || echo false)" emit julia "$( { [[ "$service" = true ]] || matches "$julia_pattern"; } && echo true || echo false)"