From fb4bd5cd925d0f037e74e479692eb1c031dba2d9 Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Wed, 12 Aug 2026 20:31:39 +0530 Subject: [PATCH 1/2] feat(release): unify product versioning --- .github/workflows/release.yml | 28 +++++++++++++++++-- CHANGELOG.md | 11 ++++++-- README.md | 4 +++ apps/headless/Dockerfile.linux | 16 ++++++++--- apps/headless/MCP/main.swift | 2 +- apps/headless/Package.swift | 20 ++++++++++++- apps/headless/Sources/HeadlessCLI/main.swift | 2 ++ .../Sources/HeadlessProtocol/CLI.swift | 5 ++++ .../Sources/HeadlessProtocol/HostCore.swift | 1 + .../HeadlessProtocol/ProductVersion.swift | 5 ++++ .../Tests/HeadlessMCPTests/main.swift | 5 ++-- .../HeadlessProtocolTests/ProtocolTests.swift | 4 +++ apps/headless/VERSION | 1 + .../VersionSupport/headless_version.c | 9 ++++++ .../VersionSupport/include/headless_version.h | 6 ++++ apps/headless/build-linux.sh | 5 ++-- apps/headless/build.sh | 2 +- apps/headless/package.json | 2 +- apps/headless/test.sh | 15 +++++++++- apps/web/package.json | 2 +- docs/roadmap/architecture-decisions.md | 4 ++- docs/roadmap/improvements-backlog.md | 2 +- package.json | 1 + 23 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 apps/headless/Sources/HeadlessProtocol/ProductVersion.swift create mode 100644 apps/headless/VERSION create mode 100644 apps/headless/VersionSupport/headless_version.c create mode 100644 apps/headless/VersionSupport/include/headless_version.h diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d57ba5..755e1d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,21 @@ jobs: outputs: version: ${{ steps.version.outputs.version }} steps: + - uses: actions/checkout@v7 - id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + shell: bash + run: | + version="${GITHUB_REF_NAME#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "Release tag must be a semantic version prefixed with v: $GITHUB_REF_NAME" >&2 + exit 64 + fi + expected="$(tr -d '[:space:]' < apps/headless/VERSION)" + if [[ "$version" != "$expected" ]]; then + echo "Release tag $version does not match apps/headless/VERSION $expected" >&2 + exit 64 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" macos: needs: version @@ -29,6 +42,8 @@ jobs: HEADLESS_VERSION: ${{ needs.version.outputs.version }} run: ./apps/headless/build.sh - name: Unit tests + env: + HEADLESS_VERSION: ${{ needs.version.outputs.version }} run: ./apps/headless/test.sh - name: E2E run: zsh ./apps/headless/Tests/macos-e2e.sh @@ -49,7 +64,10 @@ jobs: steps: - uses: actions/checkout@v7 - name: Build - run: HEADLESS_LINUX_PLATFORM=linux/amd64 ./apps/headless/build-linux.sh + env: + HEADLESS_LINUX_PLATFORM: linux/amd64 + HEADLESS_VERSION: ${{ needs.version.outputs.version }} + run: ./apps/headless/build-linux.sh - name: E2E run: ./apps/headless/Tests/linux-docker.sh - name: Package @@ -69,7 +87,10 @@ jobs: steps: - uses: actions/checkout@v7 - name: Build - run: HEADLESS_LINUX_PLATFORM=linux/arm64 ./apps/headless/build-linux.sh + env: + HEADLESS_LINUX_PLATFORM: linux/arm64 + HEADLESS_VERSION: ${{ needs.version.outputs.version }} + run: ./apps/headless/build-linux.sh - name: E2E run: ./apps/headless/Tests/linux-docker.sh - name: Package @@ -98,6 +119,7 @@ jobs: with: tag_name: ${{ github.ref_name }} name: Headless ${{ needs.version.outputs.version }} + generate_release_notes: true body: | ## Downloads diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6f257..a7e7ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,10 @@ Releases are tagged `vX.Y.Z` and published by Two versions travel independently, on purpose: -- **Product version** โ€” the git tag, flowing into the macOS `Info.plist` via - `HEADLESS_VERSION` and into release assets. -- **Protocol version** โ€” `headlessProtocolVersion` in `Protocol.swift`, +- **Product version**: the git tag, embedded in every binary via + `HEADLESS_VERSION`, reported by the CLI, host, and MCP adapter, and used for + release assets. +- **Protocol version**: `headlessProtocolVersion` in `Protocol.swift`, currently `0.5`. It changes only when the wire contract changes, and always with an entry in [`docs/roadmap/architecture-decisions.md`](docs/roadmap/architecture-decisions.md). @@ -72,6 +73,10 @@ Cutting that release is tracked in ### Changed +- Product versions now come from the release tag at build time and are + reported consistently by `headless --version`, host `ping`, MCP + `serverInfo`, package metadata, and the website. Release notes are generated + automatically for each tag while protocol versioning stays independent. - macOS WebKit and Linux Chromium now share one `HostCore` dispatcher and lifecycle implementation behind small engine adapters, eliminating the two divergent copies of flow, capture, recording, report, trace, and error logic. diff --git a/README.md b/README.md index 870c5eb..cf17e58 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,10 @@ git tag v1.0.0 git push origin v1.0.0 ``` +The tag is embedded as the product version in every binary. Verify an install +with `headless --version`; wire protocol compatibility is versioned +independently. See [CHANGELOG.md](CHANGELOG.md) for release history. + Assets: macOS `Headless.app` zip, Linux amd64/arm64 tarballs. See the Actions `Release` workflow and the release notes on each tag for install caveats (Gatekeeper; Linux Chromium/FFmpeg). diff --git a/apps/headless/Dockerfile.linux b/apps/headless/Dockerfile.linux index 9cce5e4..a70acb0 100644 --- a/apps/headless/Dockerfile.linux +++ b/apps/headless/Dockerfile.linux @@ -1,6 +1,10 @@ FROM swift:6.3-bookworm AS builder WORKDIR /src +ARG HEADLESS_VERSION +ENV HEADLESS_BUILD_VERSION=${HEADLESS_VERSION} COPY Package.swift ./ +COPY VERSION ./ +COPY VersionSupport ./VersionSupport COPY main.swift ./ COPY Host ./Host COPY Sources ./Sources @@ -11,11 +15,15 @@ COPY docs ./docs COPY tools ./tools COPY build.sh build-linux.sh install-linux.sh benchmark.sh test.sh package.json headless.entitlements Dockerfile.linux .dockerignore ./ RUN mkdir -p build Headless.app -RUN swift build -c release --static-swift-stdlib --product headless-protocol-tests \ +RUN HEADLESS_VERSION="${HEADLESS_BUILD_VERSION:-$(cat VERSION)}" \ + swift build -c release --static-swift-stdlib --product headless-protocol-tests \ && ./.build/release/headless-protocol-tests -RUN swift build -c release --static-swift-stdlib --product headless -RUN swift build -c release --static-swift-stdlib --product headless-linux-host -RUN swift build -c release --static-swift-stdlib --product headless-mcp +RUN HEADLESS_VERSION="${HEADLESS_BUILD_VERSION:-$(cat VERSION)}" \ + swift build -c release --static-swift-stdlib --product headless +RUN HEADLESS_VERSION="${HEADLESS_BUILD_VERSION:-$(cat VERSION)}" \ + swift build -c release --static-swift-stdlib --product headless-linux-host +RUN HEADLESS_VERSION="${HEADLESS_BUILD_VERSION:-$(cat VERSION)}" \ + swift build -c release --static-swift-stdlib --product headless-mcp RUN strip --strip-unneeded .build/release/headless .build/release/headless-linux-host .build/release/headless-mcp FROM debian:bookworm-slim AS runtime-base diff --git a/apps/headless/MCP/main.swift b/apps/headless/MCP/main.swift index 8620479..a977edd 100644 --- a/apps/headless/MCP/main.swift +++ b/apps/headless/MCP/main.swift @@ -52,7 +52,7 @@ while let line = readLine() { write(["jsonrpc": "2.0", "id": id ?? NSNull(), "result": [ "protocolVersion": "2025-06-18", "capabilities": ["tools": ["listChanged": false]], - "serverInfo": ["name": "headless", "version": headlessProtocolVersion], + "serverInfo": ["name": "headless", "version": headlessProductVersion], ]]) case "notifications/initialized": continue diff --git a/apps/headless/Package.swift b/apps/headless/Package.swift index 66278fd..5f27c87 100644 --- a/apps/headless/Package.swift +++ b/apps/headless/Package.swift @@ -1,6 +1,17 @@ // swift-tools-version: 5.10 import PackageDescription +import Foundation + +let packageDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() +let fallbackProductVersion = try String( + contentsOf: packageDirectory.appendingPathComponent("VERSION"), encoding: .utf8 +).trimmingCharacters(in: .whitespacesAndNewlines) +let productVersion = ProcessInfo.processInfo.environment["HEADLESS_VERSION"] ?? fallbackProductVersion +let semanticVersionPattern = #"^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$"# +guard productVersion.range(of: semanticVersionPattern, options: .regularExpression) != nil else { + fatalError("HEADLESS_VERSION must be a semantic version, received: \(productVersion)") +} let package = Package( name: "Headless", @@ -15,8 +26,15 @@ let package = Package( .library(name: "HeadlessProtocol", targets: ["HeadlessProtocol"]), ], targets: [ + .target( + name: "CHeadlessVersion", + path: "VersionSupport", + publicHeadersPath: "include", + cSettings: [.define("HEADLESS_PRODUCT_VERSION", to: "\"\(productVersion)\"")] + ), .target( name: "HeadlessProtocol", + dependencies: ["CHeadlessVersion"], resources: [.process("Resources")] ), .executableTarget( @@ -38,7 +56,7 @@ let package = Package( dependencies: ["HeadlessProtocol"], path: ".", exclude: [ - "Package.swift", "Sources", "Tests", "tools", "build.sh", + "Package.swift", "Sources", "Tests", "tools", "VersionSupport", "VERSION", "build.sh", "package.json", "headless.entitlements", "build", "docs", "test.sh", "LinuxHost", "Dockerfile.linux", "Headless.app", "build-linux.sh", "install-linux.sh", "benchmark.sh", ".dockerignore", "MCP", "node_modules", diff --git a/apps/headless/Sources/HeadlessCLI/main.swift b/apps/headless/Sources/HeadlessCLI/main.swift index ea1d886..816cee6 100644 --- a/apps/headless/Sources/HeadlessCLI/main.swift +++ b/apps/headless/Sources/HeadlessCLI/main.swift @@ -107,6 +107,8 @@ do { switch local { case .help: print(agentHelp) + case .version: + print("headless \(headlessProductVersion)") case .capabilities: printJSON(capabilitiesDocument) case .runtime: diff --git a/apps/headless/Sources/HeadlessProtocol/CLI.swift b/apps/headless/Sources/HeadlessProtocol/CLI.swift index 5942dff..e4232f2 100644 --- a/apps/headless/Sources/HeadlessProtocol/CLI.swift +++ b/apps/headless/Sources/HeadlessProtocol/CLI.swift @@ -2,6 +2,7 @@ import Foundation public enum LocalCommand: Equatable, Sendable { case help + case version case capabilities case runtime case start @@ -74,6 +75,9 @@ public struct CLIParser { case "help", "--help", "-h": try requireEmpty(arguments) return CLIInvocation(local: .help, jsonOutput: jsonOutput) + case "version", "--version", "-V": + try requireEmpty(arguments) + return CLIInvocation(local: .version, jsonOutput: jsonOutput) case "capabilities": try requireEmpty(arguments) return CLIInvocation(local: .capabilities, jsonOutput: true) @@ -638,6 +642,7 @@ Core workflow: headless --session qa capture-info Commands: + version | --version start | status | stop | runtime session create [NAME] | session list | session close NAME visit URL diff --git a/apps/headless/Sources/HeadlessProtocol/HostCore.swift b/apps/headless/Sources/HeadlessProtocol/HostCore.swift index e68d723..772e937 100644 --- a/apps/headless/Sources/HeadlessProtocol/HostCore.swift +++ b/apps/headless/Sources/HeadlessProtocol/HostCore.swift @@ -234,6 +234,7 @@ public final class HostCore: @unchecked Sendable { "pid": .number(Double(ProcessInfo.processInfo.processIdentifier)), "engine": .string(engine.name), "platform": .string(engine.platform), + "productVersion": .string(headlessProductVersion), "protocolVersion": .string(headlessProtocolVersion), "capabilities": engine.capabilities.document, "recordingAvailable": .bool(BrowserRecording.isAvailable()), diff --git a/apps/headless/Sources/HeadlessProtocol/ProductVersion.swift b/apps/headless/Sources/HeadlessProtocol/ProductVersion.swift new file mode 100644 index 0000000..3c0760c --- /dev/null +++ b/apps/headless/Sources/HeadlessProtocol/ProductVersion.swift @@ -0,0 +1,5 @@ +import CHeadlessVersion + +/// The product release version embedded at compile time. This is independent +/// from the wire protocol version used for compatibility checks. +public let headlessProductVersion = String(cString: headless_product_version()) diff --git a/apps/headless/Tests/HeadlessMCPTests/main.swift b/apps/headless/Tests/HeadlessMCPTests/main.swift index a1fbd40..41b6a15 100644 --- a/apps/headless/Tests/HeadlessMCPTests/main.swift +++ b/apps/headless/Tests/HeadlessMCPTests/main.swift @@ -24,8 +24,8 @@ func integer(_ value: Any?, _ message: String) throws -> Int { } func run() throws { - guard CommandLine.arguments.count == 2 else { - throw TestFailure(description: "usage: headless-mcp-tests /path/to/headless-mcp") + guard CommandLine.arguments.count == 3 else { + throw TestFailure(description: "usage: headless-mcp-tests /path/to/headless-mcp EXPECTED_VERSION") } try LocalRuntime.preparePrivateDirectory() @@ -84,6 +84,7 @@ func run() throws { try expect(initialize["protocolVersion"] as? String == "2025-06-18", "initialize protocol version changed") let serverInfo = try object(initialize["serverInfo"], "initialize server info was absent") try expect(serverInfo["name"] as? String == "headless", "initialize server name changed") + try expect(serverInfo["version"] as? String == CommandLine.arguments[2], "MCP product version changed") let list = try object(responses[1]["result"], "tools/list result was absent") guard let tools = list["tools"] as? [[String: Any]], tools.count == 1 else { diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift index e2f6436..91f3197 100644 --- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift +++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift @@ -725,6 +725,9 @@ struct ProtocolTests { (["start"], .start), (["help"], .help), (["--help"], .help), + (["version"], .version), + (["--version"], .version), + (["-V"], .version), ] for (arguments, command) in localCommands { let invocation = try CLIParser().parse(arguments) @@ -1819,6 +1822,7 @@ struct ProtocolTests { } try expect(pingResult["engine"] == .string("fake"), "ping should identify the engine") try expect(pingResult["platform"] == .string("test"), "ping should identify the platform") + try expect(pingResult["productVersion"] == .string(headlessProductVersion), "ping should identify the product version") try expect(pingResult["adapter"] == .string("test-adapter"), "engine ping details should be merged") try expect(pingResult["capabilities"] != nil, "ping should publish the active engine profile") diff --git a/apps/headless/VERSION b/apps/headless/VERSION new file mode 100644 index 0000000..6d7de6e --- /dev/null +++ b/apps/headless/VERSION @@ -0,0 +1 @@ +1.0.2 diff --git a/apps/headless/VersionSupport/headless_version.c b/apps/headless/VersionSupport/headless_version.c new file mode 100644 index 0000000..90a6f35 --- /dev/null +++ b/apps/headless/VersionSupport/headless_version.c @@ -0,0 +1,9 @@ +#include "headless_version.h" + +#ifndef HEADLESS_PRODUCT_VERSION +#error "HEADLESS_PRODUCT_VERSION must be defined by Package.swift" +#endif + +const char *headless_product_version(void) { + return HEADLESS_PRODUCT_VERSION; +} diff --git a/apps/headless/VersionSupport/include/headless_version.h b/apps/headless/VersionSupport/include/headless_version.h new file mode 100644 index 0000000..a55c263 --- /dev/null +++ b/apps/headless/VersionSupport/include/headless_version.h @@ -0,0 +1,6 @@ +#ifndef HEADLESS_VERSION_H +#define HEADLESS_VERSION_H + +const char *headless_product_version(void); + +#endif diff --git a/apps/headless/build-linux.sh b/apps/headless/build-linux.sh index 351c12b..a79de49 100755 --- a/apps/headless/build-linux.sh +++ b/apps/headless/build-linux.sh @@ -9,10 +9,11 @@ command -v docker >/dev/null 2>&1 || { } IMAGE="headless-linux-build" +VERSION="${HEADLESS_VERSION:-$(tr -d '[:space:]' < VERSION)}" if [ -n "${HEADLESS_LINUX_PLATFORM:-}" ]; then - docker build --platform "$HEADLESS_LINUX_PLATFORM" --target production -f Dockerfile.linux -t "$IMAGE" . + docker build --build-arg HEADLESS_VERSION="$VERSION" --platform "$HEADLESS_LINUX_PLATFORM" --target production -f Dockerfile.linux -t "$IMAGE" . else - docker build --target production -f Dockerfile.linux -t "$IMAGE" . + docker build --build-arg HEADLESS_VERSION="$VERSION" --target production -f Dockerfile.linux -t "$IMAGE" . fi mkdir -p build/linux CONTAINER="$(docker create "$IMAGE")" diff --git a/apps/headless/build.sh b/apps/headless/build.sh index b6d0a50..8bb47b0 100755 --- a/apps/headless/build.sh +++ b/apps/headless/build.sh @@ -13,7 +13,7 @@ done APP="Headless.app" ARCH="$(uname -m)" ICON="build/Headless.icns" -VERSION="${HEADLESS_VERSION:-1.0.0}" +VERSION="${HEADLESS_VERSION:-$(tr -d '[:space:]' < VERSION)}" mkdir -p build/module-cache build/swiftpm-module-cache build/bin # Select an SDK the installed Swift compiler can read. Apple occasionally ships diff --git a/apps/headless/package.json b/apps/headless/package.json index 7e6dd27..7b7e646 100644 --- a/apps/headless/package.json +++ b/apps/headless/package.json @@ -1,7 +1,7 @@ { "name": "@headless/app", "private": true, - "version": "0.0.0", + "version": "1.0.2", "scripts": { "build": "./build.sh", "build:linux": "./build-linux.sh", diff --git a/apps/headless/test.sh b/apps/headless/test.sh index 65ea996..528123b 100755 --- a/apps/headless/test.sh +++ b/apps/headless/test.sh @@ -39,9 +39,22 @@ fi TEST_SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/headless-tests.XXXXXX")" trap 'rm -rf "$TEST_SCRATCH"' EXIT +EXPECTED_VERSION="${HEADLESS_VERSION:-$(tr -d '[:space:]' < VERSION)}" +for manifest in package.json ../web/package.json ../../package.json; do + MANIFEST_VERSION="$(sed -n 's/^[[:space:]]*"version": "\([^"]*\)",*$/\1/p' "$manifest")" + [[ "$MANIFEST_VERSION" == "$(tr -d '[:space:]' < VERSION)" ]] || { + echo "headless tests: $manifest version does not match VERSION" >&2 + exit 1 + } +done BIN_PATH="$(swift build "${SDK_ARGS[@]}" --scratch-path "$TEST_SCRATCH" --show-bin-path)" swift build "${SDK_ARGS[@]}" --product headless-protocol-tests --scratch-path "$TEST_SCRATCH" +swift build "${SDK_ARGS[@]}" --product headless --scratch-path "$TEST_SCRATCH" swift build "${SDK_ARGS[@]}" --product headless-mcp --scratch-path "$TEST_SCRATCH" swift build "${SDK_ARGS[@]}" --product headless-mcp-tests --scratch-path "$TEST_SCRATCH" "$BIN_PATH/headless-protocol-tests" -"$BIN_PATH/headless-mcp-tests" "$BIN_PATH/headless-mcp" +[[ "$("$BIN_PATH/headless" --version)" == "headless $EXPECTED_VERSION" ]] || { + echo "headless tests: CLI product version does not match $EXPECTED_VERSION" >&2 + exit 1 +} +"$BIN_PATH/headless-mcp-tests" "$BIN_PATH/headless-mcp" "$EXPECTED_VERSION" diff --git a/apps/web/package.json b/apps/web/package.json index 763f0d7..28ee7c5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@headless/web", "private": true, - "version": "0.1.0", + "version": "1.0.2", "scripts": { "dev": "next dev", "build": "next build", diff --git a/docs/roadmap/architecture-decisions.md b/docs/roadmap/architecture-decisions.md index 27edb27..1f98727 100644 --- a/docs/roadmap/architecture-decisions.md +++ b/docs/roadmap/architecture-decisions.md @@ -234,6 +234,8 @@ WKWebView engine would declare `UNSUPPORTED_CAPABILITY` or use non-persistent ## 12. Versioning: unify on the git tag (change, Phase 3) +**Status:** implemented 2026-08-12. + **Decision:** the git tag becomes the single version source: injected at build time (already works via `HEADLESS_VERSION`), reported by a new `headless --version`/`version` command and in `ping`, matched by `package.json`, MCP @@ -359,7 +361,7 @@ override, the 500-event bound, and truncation reporting. | 5 | Remote stays SSH-only; no cloud offering | Decided (owner) | 2026-08-04 | | 6 | Windows = stretch via Chromium engine; WSL2/Docker interim | Decided (owner) | 2026-08-04 | | 8 | Real CDP input on Linux as capability upgrade | Planned (Phase 4) | 2026-08-04 | -| 12 | Version unification on git tag | Planned (Phase 3) | 2026-08-04 | +| 12 | Version unification on git tag | Implemented | 2026-08-04 | | 14 | Run one conformance scenario against every engine | Implemented | 2026-08-10 | | 15 | Package-manager distribution set | Decided (owner) | 2026-08-04 | | 16 | Preserve CLI value boundaries with `--` and shell quoting | Decided | 2026-08-10 | diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index 766d6f2..a23ce6c 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -371,7 +371,7 @@ Owner-decided scope: package managers, no hosted service. - **E4.** ([#42](https://github.com/LockInTime/headless/issues/42)) `SHA256SUMS` (+ consider cosign) for all release assets โ€” the QA evidence bundle already ships sums; releases don't. - **E5.** ([#43](https://github.com/LockInTime/headless/issues/43)) npm wrapper package (binary download shim) for `npx` reach. -- **E6.** ([#44](https://github.com/LockInTime/headless/issues/44)) Version unification + `headless --version` + CHANGELOG + release +- **E6.** [x] ([#44](https://github.com/LockInTime/headless/issues/44)) Version unification + `headless --version` + CHANGELOG + release automation (architecture ยง12). `package.json` says 0.0.0, tags say 1.0.x, default `HEADLESS_VERSION` is 1.0.0. - **E7.** ([#45](https://github.com/LockInTime/headless/issues/45)) Cut a release: everything since v1.0.2 (capture formats, context diff --git a/package.json b/package.json index 86c72c5..f3445a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "headless", "private": true, + "version": "1.0.2", "scripts": { "build": "pnpm --filter @headless/app build && pnpm --filter @headless/web build", "build:linux": "pnpm --filter @headless/app build:linux", From ab75271ced86b18639d4f1747b273244264d25d9 Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Wed, 12 Aug 2026 21:24:51 +0530 Subject: [PATCH 2/2] fix(release): enforce strict semantic versions --- .github/workflows/release.yml | 2 +- apps/headless/Package.swift | 5 ++++- apps/headless/VersionSupport/semver-pattern.txt | 1 + apps/headless/test.sh | 6 ++++++ 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 apps/headless/VersionSupport/semver-pattern.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f46ab5e..d1228c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: set -euo pipefail if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then version="${GITHUB_REF_NAME#v}" - if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + if ! printf '%s\n' "$version" | grep -Eq -f apps/headless/VersionSupport/semver-pattern.txt; then echo "Release tag must be a semantic version prefixed with v: $GITHUB_REF_NAME" >&2 exit 64 fi diff --git a/apps/headless/Package.swift b/apps/headless/Package.swift index 5f27c87..a56332a 100644 --- a/apps/headless/Package.swift +++ b/apps/headless/Package.swift @@ -8,7 +8,9 @@ let fallbackProductVersion = try String( contentsOf: packageDirectory.appendingPathComponent("VERSION"), encoding: .utf8 ).trimmingCharacters(in: .whitespacesAndNewlines) let productVersion = ProcessInfo.processInfo.environment["HEADLESS_VERSION"] ?? fallbackProductVersion -let semanticVersionPattern = #"^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$"# +let semanticVersionPattern = try String( + contentsOf: packageDirectory.appendingPathComponent("VersionSupport/semver-pattern.txt"), encoding: .utf8 +).trimmingCharacters(in: .whitespacesAndNewlines) guard productVersion.range(of: semanticVersionPattern, options: .regularExpression) != nil else { fatalError("HEADLESS_VERSION must be a semantic version, received: \(productVersion)") } @@ -29,6 +31,7 @@ let package = Package( .target( name: "CHeadlessVersion", path: "VersionSupport", + exclude: ["semver-pattern.txt"], publicHeadersPath: "include", cSettings: [.define("HEADLESS_PRODUCT_VERSION", to: "\"\(productVersion)\"")] ), diff --git a/apps/headless/VersionSupport/semver-pattern.txt b/apps/headless/VersionSupport/semver-pattern.txt new file mode 100644 index 0000000..4a91152 --- /dev/null +++ b/apps/headless/VersionSupport/semver-pattern.txt @@ -0,0 +1 @@ +^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+)(\.[0-9A-Za-z-]+)*)?$ diff --git a/apps/headless/test.sh b/apps/headless/test.sh index e7487f2..86d66a6 100755 --- a/apps/headless/test.sh +++ b/apps/headless/test.sh @@ -42,6 +42,12 @@ fi TEST_SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/headless-tests.XXXXXX")" trap 'rm -rf "$TEST_SCRATCH"' EXIT EXPECTED_VERSION="${HEADLESS_VERSION:-$(tr -d '[:space:]' < VERSION)}" +for INVALID_VERSION in 01.2.3 1.02.3 1.2.03 1.2.3-01 1.2.3-.beta 1.2.3-beta. 1.2.3+build..1; do + if HEADLESS_VERSION="$INVALID_VERSION" swift package dump-package >/dev/null 2>&1; then + echo "headless tests: invalid product version was accepted: $INVALID_VERSION" >&2 + exit 1 + fi +done for manifest in package.json ../web/package.json ../../package.json; do MANIFEST_VERSION="$(sed -n 's/^[[:space:]]*"version": "\([^"]*\)",*$/\1/p' "$manifest")" [[ "$MANIFEST_VERSION" == "$(tr -d '[:space:]' < VERSION)" ]] || {