diff --git a/.github/workflows/plugin-release-policy.yml b/.github/workflows/plugin-release-policy.yml new file mode 100644 index 000000000..14fe79f49 --- /dev/null +++ b/.github/workflows/plugin-release-policy.yml @@ -0,0 +1,31 @@ +name: Plugin Release Policy + +on: + pull_request: + paths: + - 'TypeWhisperPluginSDK/Plugins/**/manifest.json' + - 'scripts/*plugin*release*.py' + - '.github/workflows/plugin-release*.yml' + push: + branches: [main] + paths: + - 'TypeWhisperPluginSDK/Plugins/**/manifest.json' + - 'scripts/*plugin*release*.py' + - '.github/workflows/plugin-release*.yml' + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Validate new plugin release manifests + run: python3 scripts/validate_plugin_release_manifest.py TypeWhisperPluginSDK/Plugins/*/manifest.json + - name: Test release policy and minimum host resolution + run: | + python3 scripts/test_validate_plugin_release_manifest.py + python3 scripts/test_resolve_plugin_host_release.py diff --git a/.github/workflows/plugin-release.yml b/.github/workflows/plugin-release.yml index 433066f85..8a22e87c2 100644 --- a/.github/workflows/plugin-release.yml +++ b/.github/workflows/plugin-release.yml @@ -178,6 +178,17 @@ jobs: fi python3 scripts/assemble_community_plugin_registry.py + - name: Validate release manifest + run: | + MANIFEST="TypeWhisperPluginSDK/Plugins/${TARGET}/manifest.json" + if [ ! -f "$MANIFEST" ]; then + echo "Missing manifest for ${TARGET}: $MANIFEST" + exit 1 + fi + python3 scripts/validate_plugin_release_manifest.py "$MANIFEST" --version "$PLUGIN_VERSION" + echo "Validated $MANIFEST for release $PLUGIN_VERSION" + cat "$MANIFEST" + - name: Setup Xcode uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: @@ -189,59 +200,6 @@ jobs: -project TypeWhisper.xcodeproj \ -scheme TypeWhisper - - name: Validate release manifest - run: | - MANIFEST="TypeWhisperPluginSDK/Plugins/${TARGET}/manifest.json" - if [ ! -f "$MANIFEST" ]; then - echo "Missing manifest for ${TARGET}: $MANIFEST" - exit 1 - fi - export MANIFEST - python3 <<'PYEOF' - import json - import os - import re - - manifest_path = os.environ["MANIFEST"] - expected_version = os.environ["PLUGIN_VERSION"] - - with open(manifest_path) as manifest_file: - manifest = json.load(manifest_file) - - actual_version = manifest.get("version") - if actual_version != expected_version: - raise SystemExit( - f"manifest version {actual_version!r} does not match release version {expected_version!r}" - ) - - min_host_version = manifest.get("minHostVersion") - if not isinstance(min_host_version, str) or not re.fullmatch( - r"[0-9]+\.[0-9]+\.[0-9]+", - min_host_version, - ): - raise SystemExit( - f"manifest minHostVersion {min_host_version!r} must be a release version like 1.6.0" - ) - minimum_host_version = (1, 6, 0) - host_version = tuple(int(part) for part in min_host_version.split(".")) - if host_version < minimum_host_version: - raise SystemExit( - f"manifest minHostVersion {min_host_version!r} must be 1.6.0 or newer" - ) - - sdk_compatibility_version = manifest.get("sdkCompatibilityVersion") - if not isinstance(sdk_compatibility_version, str) or not re.fullmatch( - r"v[1-9][0-9]*", - sdk_compatibility_version, - ): - raise SystemExit( - "manifest sdkCompatibilityVersion " - f"{sdk_compatibility_version!r} must use the format vN" - ) - PYEOF - echo "Validated $MANIFEST for release $PLUGIN_VERSION" - cat "$MANIFEST" - - name: Build Plugin run: | EXTRA_SETTINGS=() diff --git a/TypeWhisperPluginSDK/Plugins/AssemblyAIPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/AssemblyAIPlugin/manifest.json index 88d9c9306..b85e582ad 100644 --- a/TypeWhisperPluginSDK/Plugins/AssemblyAIPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/AssemblyAIPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.assemblyai", "name": "AssemblyAI", "version": "1.1.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/CartesiaPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/CartesiaPlugin/manifest.json index c23acc308..72de3c0c6 100644 --- a/TypeWhisperPluginSDK/Plugins/CartesiaPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/CartesiaPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.cartesia", "name": "Cartesia", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/CerebrasPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/CerebrasPlugin/manifest.json index e146dbed0..f6a6373b1 100644 --- a/TypeWhisperPluginSDK/Plugins/CerebrasPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/CerebrasPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.cerebras", "name": "Cerebras", "version": "1.0.5", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/ClaudePlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/ClaudePlugin/manifest.json index 4b2310ae5..53aea1d2d 100644 --- a/TypeWhisperPluginSDK/Plugins/ClaudePlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/ClaudePlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.claude", "name": "Claude", "version": "1.1.1", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/CloudflareASRPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/CloudflareASRPlugin/manifest.json index 902149439..12f6f7be7 100644 --- a/TypeWhisperPluginSDK/Plugins/CloudflareASRPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/CloudflareASRPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.cloudflare-asr", "name": "Cloudflare ASR", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "yunluyl", diff --git a/TypeWhisperPluginSDK/Plugins/CohereLocalPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/CohereLocalPlugin/manifest.json index 0eb00ce0c..37c0db902 100644 --- a/TypeWhisperPluginSDK/Plugins/CohereLocalPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/CohereLocalPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.cohere-transcribe", "name": "Cohere Transcribe (Local)", "version": "1.0.1", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": [ diff --git a/TypeWhisperPluginSDK/Plugins/CoherePlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/CoherePlugin/manifest.json index ff8cec6ce..fe0de7f07 100644 --- a/TypeWhisperPluginSDK/Plugins/CoherePlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/CoherePlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.cohere", "name": "Cohere", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/ContributorPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/ContributorPlugin/manifest.json index 8ec6705e5..d08d6bd4d 100644 --- a/TypeWhisperPluginSDK/Plugins/ContributorPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/ContributorPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.improve", "name": "Improve TypeWhisper", "version": "0.1.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/DeepgramPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/DeepgramPlugin/manifest.json index 88373c860..42c324e1c 100644 --- a/TypeWhisperPluginSDK/Plugins/DeepgramPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/DeepgramPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.deepgram", "name": "Deepgram", "version": "1.0.15", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/ElevenLabsPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/ElevenLabsPlugin/manifest.json index eb5ccee0f..14217ba0d 100644 --- a/TypeWhisperPluginSDK/Plugins/ElevenLabsPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/ElevenLabsPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.elevenlabs", "name": "ElevenLabs", "version": "1.0.11", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/FileJobScriptPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/FileJobScriptPlugin/manifest.json index 6365b56b5..8095e70ef 100644 --- a/TypeWhisperPluginSDK/Plugins/FileJobScriptPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/FileJobScriptPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.file-job-script", "name": "File Job Script", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/FileMemoryPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/FileMemoryPlugin/manifest.json index e1d52459b..6102cd1ed 100644 --- a/TypeWhisperPluginSDK/Plugins/FileMemoryPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/FileMemoryPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.memory.file", "name": "File Memory", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/FillerWordsPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/FillerWordsPlugin/manifest.json index 19451ff49..6895a5b4d 100644 --- a/TypeWhisperPluginSDK/Plugins/FillerWordsPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/FillerWordsPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.filler-words", "name": "Filler Words", "version": "1.0.1", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/FireworksPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/FireworksPlugin/manifest.json index 11ed04833..38948073a 100644 --- a/TypeWhisperPluginSDK/Plugins/FireworksPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/FireworksPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.fireworks", "name": "Fireworks AI", "version": "1.0.6", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/GeminiPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/GeminiPlugin/manifest.json index d3cf97399..8db5d190f 100644 --- a/TypeWhisperPluginSDK/Plugins/GeminiPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/GeminiPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.gemini", "name": "Gemini", "version": "1.1.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/Gemma4Plugin/manifest.json b/TypeWhisperPluginSDK/Plugins/Gemma4Plugin/manifest.json index fd278c94b..5a56b0059 100644 --- a/TypeWhisperPluginSDK/Plugins/Gemma4Plugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/Gemma4Plugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.gemma4", "name": "Gemma 4", "version": "1.1.6", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "supportedArchitectures": [ "arm64" diff --git a/TypeWhisperPluginSDK/Plugins/GladiaPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/GladiaPlugin/manifest.json index fb553a070..0c2593ede 100644 --- a/TypeWhisperPluginSDK/Plugins/GladiaPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/GladiaPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.gladia", "name": "Gladia", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/GoogleCloudSTTPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/GoogleCloudSTTPlugin/manifest.json index dee59d9ce..396754d8a 100644 --- a/TypeWhisperPluginSDK/Plugins/GoogleCloudSTTPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/GoogleCloudSTTPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.google-cloud-stt", "name": "Google Cloud Speech-to-Text", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/GranitePlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/GranitePlugin/manifest.json index 5a13bf905..b2ec1a633 100644 --- a/TypeWhisperPluginSDK/Plugins/GranitePlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/GranitePlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.granite", "name": "Granite Speech", "version": "1.0.10", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": [ diff --git a/TypeWhisperPluginSDK/Plugins/GroqPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/GroqPlugin/manifest.json index d1bbbcc32..cb782e935 100644 --- a/TypeWhisperPluginSDK/Plugins/GroqPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/GroqPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.groq", "name": "Groq", "version": "1.0.24", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/LinearPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/LinearPlugin/manifest.json index dbf87d839..15db3621c 100644 --- a/TypeWhisperPluginSDK/Plugins/LinearPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/LinearPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.linear", "name": "Linear", "version": "1.0.10", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/LiveTranscriptPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/LiveTranscriptPlugin/manifest.json index 7ff1ca9f6..8d290a3c0 100644 --- a/TypeWhisperPluginSDK/Plugins/LiveTranscriptPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/LiveTranscriptPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.livetranscript", "name": "Live Transcript", "version": "1.0.3", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/Tests/MCPClientPluginTests.swift b/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/Tests/MCPClientPluginTests.swift index 827ce856f..e89916d47 100644 --- a/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/Tests/MCPClientPluginTests.swift +++ b/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/Tests/MCPClientPluginTests.swift @@ -27,7 +27,7 @@ final class MCPClientPluginTests: XCTestCase { XCTAssertEqual(manifest.id, "com.typewhisper.mcp-client") XCTAssertEqual(manifest.version, "0.2.1") - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.sdkCompatibilityVersion, "v1") XCTAssertEqual(manifest.category, "action") diff --git a/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/manifest.json index 0a33973fe..4106f5065 100644 --- a/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/MCPClientPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.mcp-client", "name": "MCP Client", "version": "0.2.1", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/MemPalacePlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/MemPalacePlugin/manifest.json index 0f51e5623..d1da48621 100644 --- a/TypeWhisperPluginSDK/Plugins/MemPalacePlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/MemPalacePlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.mempalace.memory", "name": "MemPalace", "version": "0.3.5", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": ["arm64"], diff --git a/TypeWhisperPluginSDK/Plugins/MistralAIPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/MistralAIPlugin/manifest.json index 58f4b464e..73cb89f72 100644 --- a/TypeWhisperPluginSDK/Plugins/MistralAIPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/MistralAIPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.minerale.mistralai", "name": "Mistral AI", "version": "1.0.5", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "MinerAle", diff --git a/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/Tests/ObsidianPluginTests.swift b/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/Tests/ObsidianPluginTests.swift index a422d84e6..052f03ae7 100644 --- a/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/Tests/ObsidianPluginTests.swift +++ b/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/Tests/ObsidianPluginTests.swift @@ -52,7 +52,7 @@ final class ObsidianPluginTests: XCTestCase { XCTAssertEqual(manifest.id, "com.typewhisper.obsidian") XCTAssertEqual(manifest.version, "1.1.0") - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.sdkCompatibilityVersion, "v1") } diff --git a/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/manifest.json index 021d6ed77..e87fa9892 100644 --- a/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/ObsidianPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.obsidian", "name": "Obsidian", "version": "1.1.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/OpenAICompatiblePlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/OpenAICompatiblePlugin/manifest.json index 94cb49916..8ccd8dc89 100644 --- a/TypeWhisperPluginSDK/Plugins/OpenAICompatiblePlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/OpenAICompatiblePlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.openai-compatible", "name": "OpenAI Compatible", "version": "1.1.8", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/OpenAIPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/OpenAIPlugin/manifest.json index e21a76c36..752d760b7 100644 --- a/TypeWhisperPluginSDK/Plugins/OpenAIPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/OpenAIPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.openai", "name": "OpenAI / ChatGPT", "version": "1.3.3", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/OpenAIVectorMemoryPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/OpenAIVectorMemoryPlugin/manifest.json index 3bcce2068..29512c216 100644 --- a/TypeWhisperPluginSDK/Plugins/OpenAIVectorMemoryPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/OpenAIVectorMemoryPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.memory.openai-vector", "name": "OpenAI Vector Memory", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/OpenRouterPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/OpenRouterPlugin/manifest.json index f349a0ed9..231d22b5e 100644 --- a/TypeWhisperPluginSDK/Plugins/OpenRouterPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/OpenRouterPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.openrouter", "name": "OpenRouter", "version": "1.1.6", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/ParakeetPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/ParakeetPlugin/manifest.json index 20e30b8bf..926e7d08c 100644 --- a/TypeWhisperPluginSDK/Plugins/ParakeetPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/ParakeetPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.parakeet", "name": "Parakeet", "version": "1.3.1", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": [ diff --git a/TypeWhisperPluginSDK/Plugins/Qwen3Plugin/manifest.json b/TypeWhisperPluginSDK/Plugins/Qwen3Plugin/manifest.json index 3ec1079a2..d41458e34 100644 --- a/TypeWhisperPluginSDK/Plugins/Qwen3Plugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/Qwen3Plugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.qwen3", "name": "Qwen3 ASR", "version": "1.1.8", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": [ diff --git a/TypeWhisperPluginSDK/Plugins/README.md b/TypeWhisperPluginSDK/Plugins/README.md index 1f1ded516..dd9a00b4e 100644 --- a/TypeWhisperPluginSDK/Plugins/README.md +++ b/TypeWhisperPluginSDK/Plugins/README.md @@ -55,7 +55,7 @@ Plugins can subscribe to events without modifying the transcription pipeline: "id": "com.yourname.plugin-id", "name": "My Plugin", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": ["arm64"], diff --git a/TypeWhisperPluginSDK/Plugins/Reson8Plugin/manifest.json b/TypeWhisperPluginSDK/Plugins/Reson8Plugin/manifest.json index 7abdf235d..62667e384 100644 --- a/TypeWhisperPluginSDK/Plugins/Reson8Plugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/Reson8Plugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.reson8", "name": "Reson8", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "Y. Vos", diff --git a/TypeWhisperPluginSDK/Plugins/SaluteSpeechPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/SaluteSpeechPlugin/manifest.json index 312594459..80360bd53 100644 --- a/TypeWhisperPluginSDK/Plugins/SaluteSpeechPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/SaluteSpeechPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.sber-salutespeech", "name": "Sber SaluteSpeech", "version": "0.1.1", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/ScriptPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/ScriptPlugin/manifest.json index b5defc4f3..f367e6763 100644 --- a/TypeWhisperPluginSDK/Plugins/ScriptPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/ScriptPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.script", "name": "Script Runner", "version": "1.1.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/SmallestAIPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/SmallestAIPlugin/manifest.json index afccdbde5..1e6fbeaa8 100644 --- a/TypeWhisperPluginSDK/Plugins/SmallestAIPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/SmallestAIPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.smallest-pulse", "name": "Smallest Pulse", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/SpeechAnalyzerPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/SpeechAnalyzerPlugin/manifest.json index 91690d4ff..86c9255ac 100644 --- a/TypeWhisperPluginSDK/Plugins/SpeechAnalyzerPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/SpeechAnalyzerPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.speechanalyzer", "name": "Apple Speech", "version": "1.0.15", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "26.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/SpeechmaticsPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/SpeechmaticsPlugin/manifest.json index 31b1d7d1d..a0a40ade5 100644 --- a/TypeWhisperPluginSDK/Plugins/SpeechmaticsPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/SpeechmaticsPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.speechmatics", "name": "Speechmatics", "version": "1.0.8", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/Tests/SupertonicPluginTests.swift b/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/Tests/SupertonicPluginTests.swift index ad4bcb44a..ed6f45167 100644 --- a/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/Tests/SupertonicPluginTests.swift +++ b/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/Tests/SupertonicPluginTests.swift @@ -5,7 +5,7 @@ import XCTest @testable import SupertonicPlugin final class SupertonicPluginTests: XCTestCase { - func testManifestDeclaresLocalTTSPluginForHost16AndArm64() throws { + func testManifestDeclaresLocalTTSPluginForHost17AndArm64() throws { let manifestURL = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() .deletingLastPathComponent() @@ -21,7 +21,7 @@ final class SupertonicPluginTests: XCTestCase { XCTAssertEqual(manifest.category, "tts") XCTAssertEqual(manifest.hosting, .local) XCTAssertEqual(manifest.requiresAPIKey, false) - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.supportedArchitectures, ["arm64"]) } diff --git a/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/manifest.json index b8344d5f2..43fc9791b 100644 --- a/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/SupertonicPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.tts.supertonic", "name": "Supertonic (Experimental)", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": [ diff --git a/TypeWhisperPluginSDK/Plugins/SystemTTSPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/SystemTTSPlugin/manifest.json index b262bac2e..6801c8025 100644 --- a/TypeWhisperPluginSDK/Plugins/SystemTTSPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/SystemTTSPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.tts.system", "name": "System Voice", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/VoxtralPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/VoxtralPlugin/manifest.json index 43a2410be..8e3a4f54e 100644 --- a/TypeWhisperPluginSDK/Plugins/VoxtralPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/VoxtralPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.voxtral", "name": "Voxtral", "version": "1.0.14", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": [ diff --git a/TypeWhisperPluginSDK/Plugins/WebhookPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/WebhookPlugin/manifest.json index 4b5ed243e..a4db79bd3 100644 --- a/TypeWhisperPluginSDK/Plugins/WebhookPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/WebhookPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.webhook", "name": "Webhook Notifications", "version": "1.1.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/Plugins/WhisperKitPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/WhisperKitPlugin/manifest.json index eb07a55bb..0a5b8029f 100644 --- a/TypeWhisperPluginSDK/Plugins/WhisperKitPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/WhisperKitPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.whisperkit", "name": "WhisperKit", "version": "1.2.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "supportedArchitectures": [ diff --git a/TypeWhisperPluginSDK/Plugins/XAIPlugin/Tests/XAIPluginTests.swift b/TypeWhisperPluginSDK/Plugins/XAIPlugin/Tests/XAIPluginTests.swift index 0d8f6ada3..0dcd3681f 100644 --- a/TypeWhisperPluginSDK/Plugins/XAIPlugin/Tests/XAIPluginTests.swift +++ b/TypeWhisperPluginSDK/Plugins/XAIPlugin/Tests/XAIPluginTests.swift @@ -90,7 +90,7 @@ final class XAIPluginTests: XCTestCase { let manifest = try JSONDecoder().decode(PluginManifest.self, from: data) XCTAssertEqual(manifest.id, "com.typewhisper.xai") - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.category, "transcription") XCTAssertEqual(manifest.categories, ["transcription", "llm", "tts"]) XCTAssertEqual(manifest.resolvedCategoryIdentifiers, ["transcription", "llm", "tts"]) diff --git a/TypeWhisperPluginSDK/Plugins/XAIPlugin/manifest.json b/TypeWhisperPluginSDK/Plugins/XAIPlugin/manifest.json index 34a6c87ff..e65c07b7c 100644 --- a/TypeWhisperPluginSDK/Plugins/XAIPlugin/manifest.json +++ b/TypeWhisperPluginSDK/Plugins/XAIPlugin/manifest.json @@ -2,7 +2,7 @@ "id": "com.typewhisper.xai", "name": "xAI / Grok", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "TypeWhisper", diff --git a/TypeWhisperPluginSDK/README.md b/TypeWhisperPluginSDK/README.md index 4e4891dcd..0be4958c5 100644 --- a/TypeWhisperPluginSDK/README.md +++ b/TypeWhisperPluginSDK/README.md @@ -21,7 +21,7 @@ Create `Contents/Resources/manifest.json` in your bundle: "id": "com.yourname.myplugin", "name": "My Plugin", "version": "1.0.0", - "minHostVersion": "1.6.0", + "minHostVersion": "1.7.0", "sdkCompatibilityVersion": "v1", "minOSVersion": "14.0", "author": "Your Name", @@ -31,10 +31,27 @@ Create `Contents/Resources/manifest.json` in your bundle: - `id` - Unique reverse-domain identifier - `principalClass` - Must match `@objc(ClassName)` on your plugin class -- `minHostVersion` - Minimum published stable TypeWhisper version required; new releases must use `1.6.0` or newer. Official releases verify the built plugin's SDK imports against the framework shipped by this exact host release. +- `minHostVersion` - Minimum published stable TypeWhisper version required; new releases must use `1.7.0` or newer. Official releases verify the built plugin's SDK imports against the framework shipped by this exact host release. - `sdkCompatibilityVersion` - Must match `PluginSDKCompatibility.currentVersion` for marketplace/external plugins - `minOSVersion` - Minimum macOS version required (plugin is skipped on older systems) +New plugin releases are built from the TypeWhisper 1.7 SDK line and require at +least TypeWhisper 1.7.0. Keep `sdkCompatibilityVersion` at `v1`; raising the host +minimum does not change the SDK compatibility line. Validate a release manifest +before building: + +```sh +python3 scripts/validate_plugin_release_manifest.py path/to/manifest.json --version 1.0.0 +``` + +The release workflow rejects older minimum hosts before building and verifies +the resulting binary against the SDK shipped by the declared host release. +Until stable 1.7.0 is published, manual preview releases require the explicit +`allow_prerelease_host` option to use a matching 1.7.0 daily host for that check. +Previously published plugin binaries and registry releases retain their original +host requirements. Use a new plugin version for a new build; preserve old release +entries so TypeWhisper 1.6 can keep selecting its newest compatible release. + ### 3. Implement the Plugin ```swift @@ -500,7 +517,7 @@ let wavData = PluginWavEncoder.encode(samples, sampleRate: 16000) | `id` | Yes | Unique reverse-domain ID (e.g. `com.yourname.myplugin`) | | `name` | Yes | Display name | | `version` | Yes | Semver string (e.g. `1.0.0`) | -| `minHostVersion` | Yes | Minimum TypeWhisper version; new releases must use `1.6.0` or newer | +| `minHostVersion` | Yes | Minimum TypeWhisper version; new releases must use `1.7.0` or newer | | `sdkCompatibilityVersion` | No | Exact plugin SDK compatibility line. Marketplace/external plugins must match `PluginSDKCompatibility.currentVersion`. | | `minOSVersion` | No | Minimum macOS version (e.g. `14.0`, `26.0`). Plugin is skipped on older systems. | | `author` | No | Author name | diff --git a/TypeWhisperTests/PluginManifestValidationTests.swift b/TypeWhisperTests/PluginManifestValidationTests.swift index 466d8b2eb..cf7c59d61 100644 --- a/TypeWhisperTests/PluginManifestValidationTests.swift +++ b/TypeWhisperTests/PluginManifestValidationTests.swift @@ -34,9 +34,9 @@ final class PluginManifestValidationTests: XCTestCase { "\(manifestURL.lastPathComponent) must declare a stable three-component host version" ) XCTAssertNotEqual( - PluginRegistryService.compareVersions(minHostVersion, "1.6.0"), + PluginRegistryService.compareVersions(minHostVersion, "1.7.0"), .orderedAscending, - "\(manifestURL.lastPathComponent) must require TypeWhisper 1.6.0 or newer" + "\(manifestURL.lastPathComponent) must require TypeWhisper 1.7.0 or newer" ) XCTAssertEqual( manifest.sdkCompatibilityVersion, @@ -102,8 +102,8 @@ final class PluginManifestValidationTests: XCTestCase { func testSourceFootageProgressPluginsDeclareCapability() throws { let manifestExpectations = [ - ("TypeWhisperPluginSDK/Plugins/WhisperKitPlugin/manifest.json", "1.6.0"), - ("TypeWhisperPluginSDK/Plugins/ParakeetPlugin/manifest.json", "1.6.0"), + ("TypeWhisperPluginSDK/Plugins/WhisperKitPlugin/manifest.json", "1.7.0"), + ("TypeWhisperPluginSDK/Plugins/ParakeetPlugin/manifest.json", "1.7.0"), ("TypeWhisperPluginSDK/Plugins/SonioxPlugin/manifest.json", "1.7.0"), ] @@ -116,7 +116,7 @@ final class PluginManifestValidationTests: XCTestCase { } } - func testWhisperKitPlugin12RequiresCompatibleHost16() throws { + func testWhisperKitPlugin12RequiresCompatibleHost17() throws { let manifestURL = TestSupport.repoRoot.appendingPathComponent( "TypeWhisperPluginSDK/Plugins/WhisperKitPlugin/manifest.json" ) @@ -124,12 +124,12 @@ final class PluginManifestValidationTests: XCTestCase { let manifest = try JSONDecoder().decode(PluginManifest.self, from: data) XCTAssertEqual(manifest.version, "1.2.0") - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.sdkCompatibilityVersion, PluginSDKCompatibility.currentVersion) XCTAssertEqual(manifest.supportedArchitectures, ["arm64"]) } - func testMLXStoragePluginReleasesRequireHost16() throws { + func testMLXStoragePluginReleasesRequireHost17() throws { let manifestExpectations = [ ("TypeWhisperPluginSDK/Plugins/Qwen3Plugin/manifest.json", "1.1.8"), ("TypeWhisperPluginSDK/Plugins/VoxtralPlugin/manifest.json", "1.0.14"), @@ -142,11 +142,11 @@ final class PluginManifestValidationTests: XCTestCase { let data = try Data(contentsOf: manifestURL) let manifest = try JSONDecoder().decode(PluginManifest.self, from: data) XCTAssertEqual(manifest.version, expectedVersion, relativePath) - XCTAssertEqual(manifest.minHostVersion, "1.6.0", relativePath) + XCTAssertEqual(manifest.minHostVersion, "1.7.0", relativePath) } } - func testCohereLocalPlugin101RequiresCompatibleHost16() throws { + func testCohereLocalPlugin101RequiresCompatibleHost17() throws { let manifestURL = TestSupport.repoRoot.appendingPathComponent( "TypeWhisperPluginSDK/Plugins/CohereLocalPlugin/manifest.json" ) @@ -154,18 +154,18 @@ final class PluginManifestValidationTests: XCTestCase { let manifest = try JSONDecoder().decode(PluginManifest.self, from: data) XCTAssertEqual(manifest.version, "1.0.1") - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.sdkCompatibilityVersion, PluginSDKCompatibility.currentVersion) XCTAssertEqual(manifest.supportedArchitectures, ["arm64"]) } - func testOpenAIPlugin133RequiresCompatibleHost16AndDeclaresCloudHosting() throws { + func testOpenAIPlugin133RequiresCompatibleHost17AndDeclaresCloudHosting() throws { let manifestURL = TestSupport.repoRoot.appendingPathComponent("TypeWhisperPluginSDK/Plugins/OpenAIPlugin/manifest.json") let data = try Data(contentsOf: manifestURL) let manifest = try JSONDecoder().decode(PluginManifest.self, from: data) XCTAssertEqual(manifest.version, "1.3.3") - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.sdkCompatibilityVersion, PluginSDKCompatibility.currentVersion) XCTAssertEqual(manifest.hosting, .cloud) XCTAssertEqual(manifest.requiresAPIKey, false) @@ -197,13 +197,13 @@ final class PluginManifestValidationTests: XCTestCase { XCTAssertEqual(manifest.sdkCompatibilityVersion, PluginSDKCompatibility.currentVersion) } - func testGroqPluginReleaseRequiresHost16() throws { + func testGroqPluginReleaseRequiresHost17() throws { let manifestURL = TestSupport.repoRoot.appendingPathComponent("TypeWhisperPluginSDK/Plugins/GroqPlugin/manifest.json") let data = try Data(contentsOf: manifestURL) let manifest = try JSONDecoder().decode(PluginManifest.self, from: data) XCTAssertEqual(manifest.version, "1.0.24") - XCTAssertEqual(manifest.minHostVersion, "1.6.0") + XCTAssertEqual(manifest.minHostVersion, "1.7.0") XCTAssertEqual(manifest.sdkCompatibilityVersion, PluginSDKCompatibility.currentVersion) } diff --git a/TypeWhisperTests/PluginRegistryServiceTests.swift b/TypeWhisperTests/PluginRegistryServiceTests.swift index c75c7e19e..39635a5c7 100644 --- a/TypeWhisperTests/PluginRegistryServiceTests.swift +++ b/TypeWhisperTests/PluginRegistryServiceTests.swift @@ -191,6 +191,49 @@ final class PluginRegistryServiceTests: XCTestCase { XCTAssertEqual(plugins14.first?.downloadURL, "https://example.com/requires-1.4.zip") } + func testNew17PluginReleasePreservesCompatibleUpdateFor16Host() throws { + let data = Data( + """ + { + "schemaVersion": 2, + "plugins": [{ + "id": "com.typewhisper.example", + "name": "Example", + "author": "TypeWhisper", + "description": "Host-gated plugin updates.", + "category": "transcription", + "releases": [ + { + "version": "2.0.0", + "minHostVersion": "1.7.0", + "sdkCompatibilityVersion": "v1", + "size": 20, + "downloadURL": "https://example.com/requires-1.7.zip" + }, + { + "version": "1.9.0", + "minHostVersion": "1.6.0", + "sdkCompatibilityVersion": "v1", + "size": 10, + "downloadURL": "https://example.com/compatible-1.6.zip" + } + ] + }] + } + """.utf8 + ) + let response = try JSONDecoder().decode(PluginRegistryResponse.self, from: data) + let legacy = response.resolvedPlugins(appVersion: "1.6.0", sdkCompatibilityVersion: "v1") + let current = response.resolvedPlugins(appVersion: "1.7.0", sdkCompatibilityVersion: "v1") + + XCTAssertEqual(legacy.count, 1) + XCTAssertEqual(legacy.first?.version, "1.9.0") + XCTAssertEqual(legacy.first?.downloadURL, "https://example.com/compatible-1.6.zip") + XCTAssertEqual(current.count, 1) + XCTAssertEqual(current.first?.version, "2.0.0") + XCTAssertEqual(current.first?.downloadURL, "https://example.com/requires-1.7.zip") + } + func testRegistryEntryDecodesMultipleCategoryIdentifiers() throws { let data = Data( """ diff --git a/docs/release-readiness.md b/docs/release-readiness.md index dcfa682cb..0613a211b 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -4,8 +4,8 @@ This document defines the release and maintenance gates for the current `1.x` product path, with stable `1.6.0` as its published baseline. TypeWhisper `1.x` is a stable direct-download release line for macOS. The Mac -App Store remains out of scope. The `main` branch is the current `1.6` -development and maintenance line. The `release/1.5` branch is retained only for +App Store remains out of scope. The `main` branch is the current `1.7` +development line. The `release/1.5` branch is retained only for explicitly approved legacy backports; it is no longer the stable baseline. ## Audience @@ -36,14 +36,17 @@ These surfaces remain part of `1.x`, but they are positioned as advanced or auto - Widgets - Watch Folder -## `1.6` Maintenance Focus +## `1.7` Development and `1.6` Compatibility -- Keep `main` on the current `1.6` version line; daily builds currently publish - as `v1.6.0-daily.*`. +- Keep `main` on the current `1.7` version line; daily builds publish + as `v1.7.0-daily.*`. - Treat stable `1.6.0` as the release baseline and `release/1.5` as a legacy backport branch only. - Preserve the `1.x` stability contracts for the HTTP API, CLI, plugin SDK, widgets, and watch folders. -- Avoid raising plugin `minHostVersion` values to `1.6.0` unless a plugin genuinely requires new host APIs. +- New plugin release builds require `minHostVersion: "1.7.0"` or newer and use + the 1.7 SDK line. Preserve published plugin binaries and their historical + registry entries so 1.6 hosts retain compatible releases. Keep + `sdkCompatibilityVersion` unchanged unless the SDK compatibility contract changes. - Keep release-channel behavior stable: RC and daily builds are prereleases, while Homebrew and stable website messaging update only at the final stable tag. - Keep tagged app releases on the no-iCloud distribution path until the production container and Developer ID provisioning profile are approved and validated. Tag pushes matching `v*` therefore build without automatic private iCloud sync; scheduled builds continue to use `MACOS_SCHEDULED_WITHOUT_ICLOUD`, and manual dispatches continue to use the explicit `without_icloud` input. diff --git a/scripts/pr-preflight.sh b/scripts/pr-preflight.sh index aa875adf1..415c204a2 100755 --- a/scripts/pr-preflight.sh +++ b/scripts/pr-preflight.sh @@ -43,6 +43,10 @@ python3 scripts/test_plugin_registry_metadata.py python3 scripts/test_resolve_plugin_host_release.py python3 scripts/test_verify_appcast_publication.py +log "checking plugin release policy" +python3 scripts/validate_plugin_release_manifest.py TypeWhisperPluginSDK/Plugins/*/manifest.json +python3 scripts/test_validate_plugin_release_manifest.py + log "checking main-app localization completeness" python3 scripts/check_localization_completeness.py diff --git a/scripts/resolve_plugin_host_release.py b/scripts/resolve_plugin_host_release.py index 76ed54ae7..f37269789 100644 --- a/scripts/resolve_plugin_host_release.py +++ b/scripts/resolve_plugin_host_release.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import json import re diff --git a/scripts/test_validate_plugin_release_manifest.py b/scripts/test_validate_plugin_release_manifest.py new file mode 100644 index 000000000..277b09c99 --- /dev/null +++ b/scripts/test_validate_plugin_release_manifest.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Regression tests for the minimum host of new plugin releases.""" + +import copy +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from validate_plugin_release_manifest import validate_manifest + + +class PluginReleaseManifestTests(unittest.TestCase): + def manifest(self, host="1.7.0"): + return {"version": "1.2.3", "minHostVersion": host, "sdkCompatibilityVersion": "v1"} + + def test_rejects_older_hosts(self): + for host in ("1.5.0", "1.6.0", "1.6.99"): + with self.subTest(host=host), self.assertRaisesRegex(ValueError, "1.7.0 or newer"): + validate_manifest(self.manifest(host), "1.2.3") + + def test_accepts_current_and_newer_hosts(self): + for host in ("1.7.0", "1.7.1", "1.10.0", "2.0.0"): + with self.subTest(host=host): + validate_manifest(self.manifest(host), "1.2.3") + + def test_prerelease_permission_does_not_change_manifest_version_format(self): + for host in (None, 1.7, "1.7", "v1.7.0", "1.7.0-daily.20260908"): + with self.subTest(host=host), self.assertRaisesRegex(ValueError, "release version"): + validate_manifest(self.manifest(host)) + + def test_release_version_must_match(self): + with self.assertRaisesRegex(ValueError, "does not match release version"): + validate_manifest(self.manifest(), "1.2.4") + + def test_sdk_compatibility_line_remains_required(self): + for sdk in (None, "1.7.0", "v0", "v1-beta", "v2", "v10"): + manifest = self.manifest() + manifest["sdkCompatibilityVersion"] = sdk + with self.subTest(sdk=sdk), self.assertRaisesRegex(ValueError, "must be 'v1'"): + validate_manifest(manifest) + + def test_validation_does_not_rewrite_metadata(self): + manifest = self.manifest() + original = copy.deepcopy(manifest) + validate_manifest(manifest) + self.assertEqual(manifest, original) + + def test_cli_checks_every_manifest_and_reports_failure(self): + with tempfile.TemporaryDirectory() as directory: + paths = [Path(directory) / name for name in ("current.json", "old.json")] + paths[0].write_text(json.dumps(self.manifest())) + paths[1].write_text(json.dumps(self.manifest("1.6.0"))) + result = subprocess.run( + [sys.executable, str(Path(__file__).with_name("validate_plugin_release_manifest.py")), + *map(str, paths)], capture_output=True, text=True + ) + self.assertEqual(result.returncode, 1) + self.assertIn("old.json", result.stderr) + self.assertIn("1.7.0 or newer", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/validate_plugin_release_manifest.py b/scripts/validate_plugin_release_manifest.py new file mode 100644 index 000000000..7b804f6a7 --- /dev/null +++ b/scripts/validate_plugin_release_manifest.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Validate manifests for newly built TypeWhisper plugin releases.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + + +MINIMUM_HOST_VERSION = (1, 7, 0) + + +def validate_manifest(manifest: dict, expected_version: str | None = None) -> None: + if expected_version is not None and manifest.get("version") != expected_version: + raise ValueError( + f"manifest version {manifest.get('version')!r} does not match " + f"release version {expected_version!r}" + ) + + minimum = ".".join(str(part) for part in MINIMUM_HOST_VERSION) + min_host_version = manifest.get("minHostVersion") + if not isinstance(min_host_version, str) or not re.fullmatch( + r"[0-9]+\.[0-9]+\.[0-9]+", min_host_version + ): + raise ValueError( + f"manifest minHostVersion {min_host_version!r} must be a release version like {minimum}" + ) + if tuple(int(part) for part in min_host_version.split(".")) < MINIMUM_HOST_VERSION: + raise ValueError( + f"manifest minHostVersion {min_host_version!r} must be {minimum} or newer" + ) + + sdk_version = manifest.get("sdkCompatibilityVersion") + if sdk_version != "v1": + raise ValueError( + f"manifest sdkCompatibilityVersion {sdk_version!r} must be 'v1'" + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("manifest", type=Path, nargs="+") + parser.add_argument("--version", help="Expected plugin release version") + args = parser.parse_args() + for path in args.manifest: + try: + manifest = json.loads(path.read_text()) + if not isinstance(manifest, dict): + raise ValueError("manifest must be a JSON object") + validate_manifest(manifest, args.version) + except (OSError, ValueError) as error: + print(f"error: {path}: {error}", file=sys.stderr) + return 1 + print(f"Validated {path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())