From cc4a53b85036a4185e643284a8a00507f6a497ea Mon Sep 17 00:00:00 2001 From: ZeldaZach Date: Sat, 23 May 2026 12:59:58 -0400 Subject: [PATCH] [Protocol] Single-source-of-truth protocol version + publish @cockatrice/protocol The protocol version is now declared once in libcockatrice_protocol/protocol_version.json. CMake reads it at configure time and emits a generated protocol_version.h exposing COCKATRICE_PROTOCOL_VERSION; remote_client.cpp and serversocketinterface.cpp both pick it up via the existing libcockatrice_protocol link. The same JSON file is bundled into a new @cockatrice/protocol npm package (scripts/package-protocol.mjs + .github/workflows/protocol-publish.yml) so TypeScript consumers (Sockatrice/webclient) can derive PROTOCOL_VERSION from the identical source instead of hand-typing the literal. The workflow dry-runs npm pack on PRs and publishes to GitHub Packages on stable releases only. --- .github/workflows/protocol-publish.yml | 67 ++++++++++ .../network/client/remote/remote_client.cpp | 3 +- libcockatrice_protocol/CMakeLists.txt | 18 +++ libcockatrice_protocol/protocol_version.h.in | 3 + libcockatrice_protocol/protocol_version.json | 3 + scripts/package-protocol.mjs | 119 ++++++++++++++++++ servatrice/src/serversocketinterface.cpp | 3 +- 7 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/protocol-publish.yml create mode 100644 libcockatrice_protocol/protocol_version.h.in create mode 100644 libcockatrice_protocol/protocol_version.json create mode 100755 scripts/package-protocol.mjs diff --git a/.github/workflows/protocol-publish.yml b/.github/workflows/protocol-publish.yml new file mode 100644 index 000000000..e98bbb1ce --- /dev/null +++ b/.github/workflows/protocol-publish.yml @@ -0,0 +1,67 @@ +name: Publish @cockatrice/protocol + +on: + release: + types: + - released # stable releases only; prereleases intentionally skipped + pull_request: + branches: + - master + paths: + - '.github/workflows/protocol-publish.yml' + - 'scripts/package-protocol.mjs' + - 'libcockatrice_protocol/**' + +concurrency: + group: "${{ github.workflow }} @ ${{ github.ref_name }}" + cancel-in-progress: ${{ github.event_name != 'release' }} + +jobs: + publish: + name: Build and publish protocol package + if: ${{ github.repository_owner == 'Cockatrice' }} + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + # No submodules; proto files live in this repo. + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://npm.pkg.github.com' + scope: '@cockatrice' + + - name: Determine package version + id: pkgver + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION="${{ github.event.release.tag_name }}" + else + VERSION="0.0.0-pr${{ github.event.pull_request.number }}" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Build package directory + run: node scripts/package-protocol.mjs --version "${{ steps.pkgver.outputs.version }}" + + - name: Dry-run pack (pull_request) + if: ${{ github.event_name == 'pull_request' }} + working-directory: build/protocol-package + run: | + npm pack + ls -la *.tgz + tar -tzf *.tgz | sort + + - name: Publish to GitHub Packages (release) + if: ${{ github.event_name == 'release' }} + working-directory: build/protocol-package + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm publish diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index 7e20f2722..5b273a629 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -21,9 +21,10 @@ #include #include #include +#include #include -static const unsigned int protocolVersion = 14; +static const unsigned int protocolVersion = COCKATRICE_PROTOCOL_VERSION; RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSettingsProvider) : AbstractClient(parent), networkSettingsProvider(_networkSettingsProvider), timeRunning(0), lastDataReceived(0), diff --git a/libcockatrice_protocol/CMakeLists.txt b/libcockatrice_protocol/CMakeLists.txt index 7dc0a0360..5eccdf236 100644 --- a/libcockatrice_protocol/CMakeLists.txt +++ b/libcockatrice_protocol/CMakeLists.txt @@ -1,5 +1,23 @@ # Top-level wrapper for the protobuf library +# Single source of truth for the network protocol version. The same JSON file is +# shipped in the @cockatrice/protocol npm package so TypeScript consumers read +# the identical value at runtime. Regex-extracted (instead of string(JSON ...)) +# so we keep the project's CMake 3.10 floor. +set(PROTOCOL_VERSION_JSON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/protocol_version.json") +file(READ "${PROTOCOL_VERSION_JSON_PATH}" PROTOCOL_VERSION_JSON) +string(REGEX MATCH "\"protocolVersion\"[ \t\r\n]*:[ \t\r\n]*([0-9]+)" _ "${PROTOCOL_VERSION_JSON}") +if(NOT CMAKE_MATCH_1) + message(FATAL_ERROR "Failed to extract protocolVersion from ${PROTOCOL_VERSION_JSON_PATH}") +endif() +set(COCKATRICE_PROTOCOL_VERSION "${CMAKE_MATCH_1}") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${PROTOCOL_VERSION_JSON_PATH}") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/protocol_version.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/libcockatrice/protocol/protocol_version.h" + @ONLY +) + add_subdirectory(libcockatrice/protocol/pb) add_library(libcockatrice_protocol STATIC) diff --git a/libcockatrice_protocol/protocol_version.h.in b/libcockatrice_protocol/protocol_version.h.in new file mode 100644 index 000000000..b32feb730 --- /dev/null +++ b/libcockatrice_protocol/protocol_version.h.in @@ -0,0 +1,3 @@ +#pragma once +// Generated by configure_file() from protocol_version.json. Do not edit. +#define COCKATRICE_PROTOCOL_VERSION @COCKATRICE_PROTOCOL_VERSION@ diff --git a/libcockatrice_protocol/protocol_version.json b/libcockatrice_protocol/protocol_version.json new file mode 100644 index 000000000..6ca32ae7d --- /dev/null +++ b/libcockatrice_protocol/protocol_version.json @@ -0,0 +1,3 @@ +{ + "protocolVersion": 14 +} diff --git a/scripts/package-protocol.mjs b/scripts/package-protocol.mjs new file mode 100755 index 000000000..ec1dbb40a --- /dev/null +++ b/scripts/package-protocol.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +// Assembles a publish-ready @cockatrice/protocol package directory from +// libcockatrice_protocol/. Run by .github/workflows/protocol-publish.yml. +// stdlib only. + +import { readdirSync, readFileSync, mkdirSync, copyFileSync, writeFileSync, rmSync, existsSync } from "node:fs"; +import { join, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REQUIRED_CORE_PROTOS = ["commands.proto", "server_message.proto", "event_server_identification.proto"]; + +function parseArgs(argv) { + const args = { version: null, outDir: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--version") args.version = argv[++i]; + else if (a.startsWith("--version=")) args.version = a.slice("--version=".length); + else if (a === "--out-dir") args.outDir = argv[++i]; + else if (a.startsWith("--out-dir=")) args.outDir = a.slice("--out-dir=".length); + else die(`unknown argument: ${a}`); + } + if (!args.version) die("missing required --version "); + return args; +} + +function die(msg) { + console.error(`package-protocol: ${msg}`); + process.exit(1); +} + +function normalizeVersion(raw) { + const stripped = raw.replace(/^v/, ""); + if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(stripped)) { + die(`version "${raw}" is not a valid semver`); + } + return stripped; +} + +const __filename = fileURLToPath(import.meta.url); +const repoRoot = resolve(dirname(__filename), ".."); +const protoSrcDir = join(repoRoot, "libcockatrice_protocol", "libcockatrice", "protocol", "pb"); +const versionJsonSrc = join(repoRoot, "libcockatrice_protocol", "protocol_version.json"); +const licenseSrc = join(repoRoot, "LICENSE"); + +const { version: rawVersion, outDir: outDirArg } = parseArgs(process.argv.slice(2)); +const pkgVersion = normalizeVersion(rawVersion); +const outDir = resolve(outDirArg ?? join(repoRoot, "build", "protocol-package")); + +if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true }); +mkdirSync(join(outDir, "pb"), { recursive: true }); + +const protoFiles = readdirSync(protoSrcDir).filter((f) => f.endsWith(".proto")); +if (protoFiles.length === 0) die(`no .proto files found in ${protoSrcDir}`); + +const missing = REQUIRED_CORE_PROTOS.filter((f) => !protoFiles.includes(f)); +if (missing.length > 0) { + die(`required core proto files missing: ${missing.join(", ")} (layout of ${protoSrcDir} changed?)`); +} + +for (const f of protoFiles) { + copyFileSync(join(protoSrcDir, f), join(outDir, "pb", f)); +} + +copyFileSync(versionJsonSrc, join(outDir, "protocol_version.json")); +copyFileSync(licenseSrc, join(outDir, "LICENSE")); + +const pkgJson = { + name: "@cockatrice/protocol", + version: pkgVersion, + description: "Cockatrice network protocol: .proto definitions and protocol version constant.", + license: "GPL-2.0-or-later", + repository: { type: "git", url: "git+https://github.com/Cockatrice/Cockatrice.git" }, + homepage: "https://github.com/Cockatrice/Cockatrice", + files: ["pb/", "protocol_version.json", "LICENSE", "README.md"], + publishConfig: { registry: "https://npm.pkg.github.com", access: "restricted" }, + exports: { + "./protocol_version.json": "./protocol_version.json", + "./pb/*.proto": "./pb/*.proto", + }, +}; +writeFileSync(join(outDir, "package.json"), JSON.stringify(pkgJson, null, 2) + "\n"); + +const readme = `# @cockatrice/protocol + +Network protocol artifacts for [Cockatrice](https://github.com/Cockatrice/Cockatrice): the \`.proto\` +definitions used by the desktop client, Servatrice, and the webclient, plus the +authoritative protocol version constant they all share. + +## Install + +\`\`\`sh +npm install @cockatrice/protocol +\`\`\` + +The package is published to GitHub Packages under the \`@cockatrice\` scope; consumers +need an \`.npmrc\` entry pointing the scope at \`https://npm.pkg.github.com\` and a +\`GITHUB_TOKEN\` with \`read:packages\`. + +## Contents + +- \`pb/*.proto\` — every protobuf schema file from \`libcockatrice_protocol\`. +- \`protocol_version.json\` — \`{ "protocolVersion": }\`. Identical to the file the + C++ build reads via \`configure_file()\`. + +## Usage (TypeScript) + +\`\`\`ts +import protocolVersionInfo from "@cockatrice/protocol/protocol_version.json" with { type: "json" }; +export const PROTOCOL_VERSION = protocolVersionInfo.protocolVersion; +\`\`\` + +Point your protobuf code-generator (e.g. buf) at \`node_modules/@cockatrice/protocol/pb\`. +`; +writeFileSync(join(outDir, "README.md"), readme); + +console.log(`package-protocol: assembled ${outDir}`); +console.log(` name: ${pkgJson.name}`); +console.log(` version: ${pkgJson.version}`); +console.log(` proto: ${protoFiles.length} files`); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index bc90a3ef1..5677e0d74 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -79,6 +79,7 @@ #include #include #include +#include #include #include #include @@ -88,7 +89,7 @@ inline Q_LOGGING_CATEGORY(AbstractServerSocketInterfaceLog, "abstract_server_soc inline Q_LOGGING_CATEGORY(TcpServerSocketInterfaceLog, "tcp_server_socket_interface"); inline Q_LOGGING_CATEGORY(WebsocketServerSocketInterfaceLog, "websocket_server_socket_interface"); -static const int protocolVersion = 14; +static const int protocolVersion = COCKATRICE_PROTOCOL_VERSION; AbstractServerSocketInterface::AbstractServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface,