diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c03940..7261c7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: rpc/go.mod + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Run cross-language and process acceptance run: | python3 -m venv "$RUNNER_TEMP/match-platform-python" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfd6e5..abd5089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 3.0.0 + +- Align the Match Platform release major with Client Protocol V3. +- Publish a canonical `VERSION` and executable version compatibility contract. +- Supersede the historical `v0.1.0` tag without changing the V3 wire semantics. + ## 0.1.0 - Extract the generic Match Platform history, core, operations, RPC runtime, diff --git a/README.md b/README.md index 5a12092..4535fd3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Engine-neutral contracts and runtimes for authoritative multiplayer games. +Current release line: **Match Platform 3.x**, paired with **Client Protocol V3**. +The canonical full release version is stored in [`VERSION`](VERSION). + This repository owns: - Client Protocol V3 envelopes and the Godot platform core; diff --git a/VERSION b/VERSION index 6e8bf73..4a36342 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +3.0.0 diff --git a/VERSIONING.md b/VERSIONING.md index bc3b9ed..1492918 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -1,15 +1,21 @@ # Versioning policy -Repository releases use SemVer. +Repository releases use SemVer, and the release major MUST equal the supported +Client Protocol major. Client Protocol V3 is therefore released as Match +Platform `3.x.y`; there is no separate `0.x` product-version namespace. -- Patch: fixes that preserve Client Protocol V3, Adapter RPC v1 and SDK behavior. -- Minor: backward-compatible capabilities, SDKs, runtimes or operations. -- Major: an incompatible public contract or artifact-layout change. +- Patch (`3.0.x`): fixes that preserve Client Protocol V3, Adapter RPC v1 and SDK behavior. +- Minor (`3.x.0`): backward-compatible capabilities, SDKs, runtimes or operations. +- Major (`4.0.0`): Client Protocol V4 and its incompatible public contract. An + artifact-layout break that requires all consumers to migrate also requires the + next protocol/repository major, even if the envelope changes only minimally. -Client Protocol, Adapter RPC, adapter package, codec and content versions remain -independent. The historical protobuf package name +Adapter RPC, adapter package, codec and content versions remain independent. +The historical protobuf package name `hersir.adapter.rpc.v1` is retained as a frozen wire ABI; it does not imply a dependency on the downstream game. Release tags are immutable. Consumers pin both a tag for human review and its -full commit SHA for resolution. +full commit SHA for resolution. A consumer MUST verify that the tag major, the +`VERSION` file, `MatchPlatformV3.PLATFORM_VERSION_MAJOR`, and +`MatchPlatformV3.ENVELOPE_VERSION` agree. diff --git a/docs/adr/0003-match-platform-v3-contract.md b/docs/adr/0003-match-platform-v3-contract.md index 2f06d99..f8ef2af 100644 --- a/docs/adr/0003-match-platform-v3-contract.md +++ b/docs/adr/0003-match-platform-v3-contract.md @@ -51,8 +51,8 @@ Every V3 message is one dictionary carrying `pv` (=3) and `t` (type). Direction server→client. Fields below are all **platform** fields; none names a game concept. ```text -hello { pv, protocol_versions, game_id, game_version, content_hash, codecs } -welcome { pv, selected_protocol, game_id, adapter_version, selected_codec, tick_rate, capabilities } +hello { pv, protocol_versions, platform_version?, game_id, game_version, content_hash, codecs } +welcome { pv, selected_protocol, platform_version?, game_id, adapter_version, selected_codec, tick_rate, capabilities } join { pv, match_selector, role, requested_slot?, auth_context } command { pv, match_id, seq, expected_tick, codec_id, payload:opaque } state { pv, match_id, tick, base_tick, seq, codec_id, payload:opaque } diff --git a/platform/match_platform_v3.gd b/platform/match_platform_v3.gd index 11725e3..f16d822 100644 --- a/platform/match_platform_v3.gd +++ b/platform/match_platform_v3.gd @@ -23,6 +23,22 @@ extends RefCounted ## game or codec is not. const ENVELOPE_VERSION := 3 +## Repository release version. Its SemVer major is deliberately identical to +## ENVELOPE_VERSION: a Client Protocol V3 frontend consumes Match Platform 3.x. +## Minor and patch releases must remain compatible with the same V3 envelope. +const PLATFORM_VERSION := "3.0.0" +const PLATFORM_VERSION_MAJOR := 3 +const WEBSOCKET_SUBPROTOCOL := "match-platform.v%d" % PLATFORM_VERSION_MAJOR + +## True when a Match Platform SemVer belongs to this protocol generation. +## This intentionally checks the major only; compatible fixes and capabilities +## may ship as 3.x without forcing coordinated client/server deployment. +static func supports_platform_version(version: String) -> bool: + var parts := version.trim_prefix("v").split(".") + return parts.size() == 3 and parts[0].is_valid_int() \ + and parts[1].is_valid_int() and parts[2].is_valid_int() \ + and int(parts[0]) == PLATFORM_VERSION_MAJOR + ## Maximum decoded envelope size the core will accept, payload included. The core ## bounds the payload by bytes only; it never parses inside it. Mirrors the legacy ## v1/v2 wire ceiling so a V3 frame rides the same transport limits during diff --git a/platform/platform_adapter_registry.gd b/platform/platform_adapter_registry.gd index 3374c39..c1f6274 100644 --- a/platform/platform_adapter_registry.gd +++ b/platform/platform_adapter_registry.gd @@ -144,6 +144,11 @@ func resolve_hello(hello: Dictionary) -> Dictionary: var selected_protocol := V3.negotiate_protocol(client_protocols, SUPPORTED_PROTOCOLS) if selected_protocol == 0: return _refuse(V3.REJECT_UNSUPPORTED_PROTOCOL, "no shared platform protocol version") + var client_platform_version := String(hello.get("platform_version", "")) + if not client_platform_version.is_empty() \ + and not V3.supports_platform_version(client_platform_version): + return _refuse(V3.REJECT_UNSUPPORTED_PROTOCOL, + "client Match Platform major does not match Client Protocol V%d" % selected_protocol) var game_id := String(hello.get("game_id", "")) if not _packages.has(game_id): @@ -184,6 +189,7 @@ func welcome_for(hello: Dictionary, capabilities: Dictionary = {}) -> Dictionary return V3.reject(StringName(resolved.get("code", V3.REJECT_MALFORMED_ENVELOPE)), String(resolved.get("detail", ""))) return V3.envelope(V3.WELCOME, { "selected_protocol": int(resolved.selected_protocol), + "platform_version": V3.PLATFORM_VERSION, "game_id": String(resolved.game_id), "adapter_version": String(resolved.adapter_version), "selected_codec": String(resolved.selected_codec), diff --git a/tests/fixtures/v3_envelope_fixtures.json b/tests/fixtures/v3_envelope_fixtures.json index b50ad8b..b6035b4 100644 --- a/tests/fixtures/v3_envelope_fixtures.json +++ b/tests/fixtures/v3_envelope_fixtures.json @@ -15,6 +15,7 @@ "pv": 3, "t": "hello", "protocol_versions": [3], + "platform_version": "3.0.0", "game_id": "gridwars", "game_version": "1.4.0", "content_hash": "sha256:9f2c0a", @@ -24,6 +25,7 @@ "pv": 3, "t": "welcome", "selected_protocol": 3, + "platform_version": "3.0.0", "game_id": "gridwars", "adapter_version": "1.4.0", "selected_codec": "gridwars.binary.1", diff --git a/tests/platform_core_test.gd b/tests/platform_core_test.gd index 73f3dfd..5358417 100644 --- a/tests/platform_core_test.gd +++ b/tests/platform_core_test.gd @@ -283,6 +283,7 @@ func _hello(overrides: Dictionary = {}) -> Dictionary: var packet := { "pv": V3.ENVELOPE_VERSION, "t": String(V3.HELLO), "protocol_versions": [V3.ENVELOPE_VERSION], "game_id": "gridwars", + "platform_version": V3.PLATFORM_VERSION, "game_version": "1.0.0", "content_hash": GridWarsAdapter.CONTENT_HASH, "codecs": ["gw-binary", "gw-json"], } @@ -314,6 +315,9 @@ func _test_pre_seat_identity_gate() -> void: var outcome := registry.resolve_hello(cases[expected]) _check(not bool(outcome.get("ok", false)) and String(outcome.get("code", "")) == String(expected), "hello is refused with '%s'" % expected) + var incompatible_platform := registry.resolve_hello(_hello({"platform_version": "4.0.0"})) + _check(String(incompatible_platform.get("code", "")) == String(V3.REJECT_UNSUPPORTED_PROTOCOL), + "hello from a different Match Platform major is refused pre-seat") # Structural failures are still structural: a hello missing a mandatory identity # field never reaches negotiation. @@ -333,6 +337,8 @@ func _test_welcome_negotiation() -> void: _check(bool(V3.validate_envelope(welcome, V3.SERVER_MESSAGES).get("ok", false)), "welcome_for builds a contract-valid welcome envelope") _check(String(welcome.get("adapter_version", "")) == "0.1.0", "welcome carries the adapter version") + _check(String(welcome.get("platform_version", "")) == V3.PLATFORM_VERSION, + "welcome identifies the server Match Platform release") _check(bool(welcome.get("capabilities", {}).get("resync", false)), "capabilities are platform-declared, not game-declared") var refusal := registry.welcome_for(_hello({"game_id": "nosuchgame"})) diff --git a/tests/v3_contract_test.gd b/tests/v3_contract_test.gd index 0541505..d305ab1 100644 --- a/tests/v3_contract_test.gd +++ b/tests/v3_contract_test.gd @@ -43,6 +43,7 @@ func _run() -> void: quit(1) return _test_valid_envelopes(fixtures) + _test_release_version() _test_invalid_envelopes(fixtures) _test_negotiation(fixtures) _test_delivery_classes(fixtures) @@ -58,6 +59,17 @@ func _run() -> void: print(" - %s" % failure) quit(1) +func _test_release_version() -> void: + var release_version := FileAccess.get_file_as_string("res://VERSION").strip_edges() + _check(release_version == V3.PLATFORM_VERSION, + "VERSION matches the executable platform version") + _check(V3.PLATFORM_VERSION_MAJOR == V3.ENVELOPE_VERSION, + "Match Platform major matches the Client Protocol major") + _check(V3.supports_platform_version("3.0.0"), "same-major platform release is compatible") + _check(V3.supports_platform_version("v3.9.7"), "v-prefixed same-major release is compatible") + _check(not V3.supports_platform_version("4.0.0"), "next-major platform release is incompatible") + _check(not V3.supports_platform_version("3.0"), "non-SemVer platform release is invalid") + func _load_fixtures() -> Dictionary: var text := FileAccess.get_file_as_string(FIXTURES_PATH) var parsed: Variant = JSON.parse_string(text) diff --git a/tools/build_release_artifacts.py b/tools/build_release_artifacts.py index 5bf7f97..085f286 100755 --- a/tools/build_release_artifacts.py +++ b/tools/build_release_artifacts.py @@ -2,6 +2,7 @@ import argparse import hashlib import json +import re import subprocess import tarfile import tempfile @@ -22,18 +23,22 @@ def main() -> None: parser.add_argument("--version", required=True) args = parser.parse_args() + version = re.sub(r"[^A-Za-z0-9._-]+", "-", args.version).strip(".-") + if not version: + raise SystemExit("version must contain at least one filename-safe character") + root = Path(__file__).resolve().parents[1] commit = subprocess.check_output( ["git", "rev-parse", "HEAD"], cwd=root, text=True ).strip() args.output.mkdir(parents=True, exist_ok=True) - archive = args.output / f"match-platform-{args.version}.tar.gz" + archive = args.output / f"match-platform-{version}.tar.gz" with tempfile.TemporaryDirectory() as temporary: source_tar = Path(temporary) / "source.tar" with source_tar.open("wb") as stream: subprocess.run( - ["git", "archive", "--format=tar", f"--prefix=match-platform-{args.version}/", "HEAD"], + ["git", "archive", "--format=tar", f"--prefix=match-platform-{version}/", "HEAD"], cwd=root, stdout=stream, check=True, @@ -53,8 +58,8 @@ def main() -> None: "spdxVersion": "SPDX-2.3", "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", - "name": f"match-platform-{args.version}", - "documentNamespace": f"https://github.com/chimerakang/match-platform/releases/{args.version}/{commit}", + "name": f"match-platform-{version}", + "documentNamespace": f"https://github.com/chimerakang/match-platform/releases/{version}/{commit}", "creationInfo": { "creators": ["Tool: match-platform-release-builder"], }, @@ -62,7 +67,7 @@ def main() -> None: { "name": "match-platform", "SPDXID": "SPDXRef-Package", - "versionInfo": args.version, + "versionInfo": version, "downloadLocation": "NOASSERTION", "filesAnalyzed": False, "checksums": [ @@ -81,7 +86,7 @@ def main() -> None: "predicate": { "buildDefinition": { "buildType": "https://github.com/chimerakang/match-platform/.github/workflows/ci.yml", - "externalParameters": {"version": args.version}, + "externalParameters": {"version": version}, "resolvedDependencies": [ { "uri": "git+https://github.com/chimerakang/match-platform",