@@ -148,15 +148,20 @@ jobs:
|
||||
preview-gate:
|
||||
name: Preview gate
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
is_preview: ${{ steps.decide.outputs.is_preview }}
|
||||
proceed: ${{ steps.decide.outputs.proceed }}
|
||||
stable_tag: ${{ steps.decide.outputs.stable_tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- id: decide
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
event="${{ github.event_name }}"
|
||||
@@ -171,6 +176,13 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "is_preview=true" >> "$GITHUB_OUTPUT"
|
||||
# Resolve once before the matrix starts so every platform stamps
|
||||
# against the same immutable Stable-channel snapshot.
|
||||
STABLE_TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)
|
||||
[[ "$STABLE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||
echo "::error::latest stable release has an invalid tag"; exit 1;
|
||||
}
|
||||
echo "stable_tag=$STABLE_TAG" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_preview=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -497,35 +509,22 @@ jobs:
|
||||
echo "APPLE_TEAM_ID=$TID"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
# Stamp each preview build with a unique, monotonically increasing semver
|
||||
# PRERELEASE so the updater actually offers it (a rolling preview that
|
||||
# always reported the static 0.3.0 never looked "newer", so no update was
|
||||
# ever delivered). Ephemeral, CI-only — never committed. Tauri reads the
|
||||
# bundle + updater version from tauri.conf.json, so rewriting it here
|
||||
# stamps the artifacts + latest.json. Under the versioning hard rule
|
||||
# (owner-set 2026-06-11) main is always last-release + 1, so BASE-N is a
|
||||
# prerelease of the NEXT version and semver-sorts ABOVE the last stable
|
||||
# (0.3.6-N > 0.3.5) — preview users naturally upgrade past stable, and
|
||||
# the Windows MSI ProductVersion (which strips the prerelease → 0.3.6)
|
||||
# is also correctly above the last stable.
|
||||
# Stamp each preview with a numeric prerelease that is strictly above the
|
||||
# latest stable release. Main may intentionally retain the released
|
||||
# version while AUTO_VERSION_BUMP is disabled; in that case the helper
|
||||
# advances the preview base by one patch so stable users can still opt in
|
||||
# and receive it. The edit is ephemeral and never committed.
|
||||
- name: Stamp preview version
|
||||
if: needs.preview-gate.outputs.is_preview == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
STABLE_TAG: ${{ needs.preview-gate.outputs.stable_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# package.json is the single source of truth; tauri.conf.json reads its
|
||||
# version from it ("version": "../package.json"), so stamping
|
||||
# package.json restamps the whole bundle.
|
||||
CONF=frontend/package.json
|
||||
BASE=$(jq -r .version "$CONF")
|
||||
# MSI/WiX requires the semver pre-release identifier to be numeric-only
|
||||
# (and <= 65535). "preview.N" hard-fails the Windows bundler, so the
|
||||
# preview stamp is BASE-N — still sorts below the stable BASE for the
|
||||
# updater, still unique per run.
|
||||
PREVIEW_VERSION="${BASE}-${{ github.run_number }}"
|
||||
tmp=$(mktemp)
|
||||
jq --arg v "$PREVIEW_VERSION" '.version = $v' "$CONF" > "$tmp"
|
||||
mv "$tmp" "$CONF"
|
||||
PREVIEW_VERSION=$(python scripts/stamp-preview-version.py \
|
||||
--package-json frontend/package.json \
|
||||
--stable-tag "$STABLE_TAG" \
|
||||
--run-number "${{ github.run_number }}")
|
||||
echo "Stamped preview version: $PREVIEW_VERSION"
|
||||
|
||||
# The rolling `preview` release is REUSED every night, and macOS updater
|
||||
|
||||
@@ -11,6 +11,7 @@ the frozen-backend fallback mirror it for their toolchains.
|
||||
**Highlights**
|
||||
|
||||
- Show estimated and measured model, dependency, cache, and temporary disk costs in the engine catalogue (#1718)
|
||||
- Preview builds now stay newer than Stable even when automatic post-release version bumps are disabled (#1762)
|
||||
- CosyVoice setup guidance now separates downloaded model files from the runtime that makes the engine available.
|
||||
- MCP tools can now keep audio out of agent context by returning files and accepting base-path-confined file inputs (#1760) — thanks @agudmund!
|
||||
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ bug to fix immediately, not backlog.
|
||||
| Channel | Source | Produced by | How to verify |
|
||||
|---|---|---|---|
|
||||
| GitHub Release: installers + signed `latest.json` (**Stable** updater channel) | the `vX.Y.Z` tag | `release.yml` on tag push | Release page has dmg (arm+intel), msi/exe, AppImage/deb, `latest.json`; body = the CHANGELOG section (not the auto-generated fallback), followed by per-platform checksums and a **Contributors** avatar strip (owner + every PR author for the tag — the `contributors-strip` job) |
|
||||
| **Preview** updater channel (rolling `preview` prerelease) | **`main` only** | `release.yml` nightly cron / manual dispatch | preview `latest.json` stamps `X.Y.Z-N` and semver-sorts above stable |
|
||||
| **Preview** updater channel (rolling `preview` prerelease) | **`main` only** | `release.yml` nightly cron / manual dispatch | preview `latest.json` uses main's version when it is ahead; otherwise it advances the stable patch, then appends `-N` so it semver-sorts above stable |
|
||||
| GHCR CUDA image: `:X.Y.Z`, `:X.Y`, `:stable` | the tag | `docker.yml` on tag push | `docker manifest inspect ghcr.io/debpalash/omnivoice-studio:X.Y.Z` |
|
||||
| GHCR ROCm image: `:X.Y.Z-rocm`, `:X.Y-rocm`, `:stable-rocm` | the tag | `docker.yml` on tag push | same, with `-rocm` suffix |
|
||||
| Docker Hub mirror of **all** the above tags | the tag | `docker.yml` (gated on `DOCKERHUB_*` secrets) | tag list at hub.docker.com/r/palashdeb/omnivoice-studio/tags |
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stamp a Preview build above the latest stable VoiceStudio release."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_VERSION_RE = re.compile(r"^(?:v)?(\d+)\.(\d+)\.(\d+)$")
|
||||
|
||||
|
||||
def _release_tuple(value: str) -> tuple[int, int, int]:
|
||||
match = _VERSION_RE.fullmatch(value.strip())
|
||||
if match is None:
|
||||
raise ValueError(f"expected a release version X.Y.Z, got {value!r}")
|
||||
return tuple(map(int, match.groups()))
|
||||
|
||||
|
||||
def preview_version(package_version: str, stable_tag: str, run_number: int) -> str:
|
||||
"""Return a numeric-prerelease SemVer strictly above ``stable_tag``."""
|
||||
if not 0 < run_number <= 65_535:
|
||||
raise ValueError("run number must be between 1 and 65535 for WiX")
|
||||
package = _release_tuple(package_version)
|
||||
stable = _release_tuple(stable_tag)
|
||||
base = package if package > stable else (stable[0], stable[1], stable[2] + 1)
|
||||
return f"{base[0]}.{base[1]}.{base[2]}-{run_number}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--package-json", type=Path, required=True)
|
||||
parser.add_argument("--stable-tag", required=True)
|
||||
parser.add_argument("--run-number", type=int, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
package = json.loads(args.package_json.read_text(encoding="utf-8"))
|
||||
stamped = preview_version(package["version"], args.stable_tag, args.run_number)
|
||||
package["version"] = stamped
|
||||
args.package_json.write_text(
|
||||
json.dumps(package, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(stamped)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = ROOT / "scripts" / "stamp-preview-version.py"
|
||||
SPEC = importlib.util.spec_from_file_location("stamp_preview_version", SCRIPT)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("package", "stable", "expected"),
|
||||
[
|
||||
("0.5.2", "v0.5.1", "0.5.2-144"),
|
||||
("0.5.2", "v0.5.2", "0.5.3-144"),
|
||||
("0.4.9", "v0.5.2", "0.5.3-144"),
|
||||
],
|
||||
)
|
||||
def test_preview_is_based_above_stable(package: str, stable: str, expected: str) -> None:
|
||||
assert MODULE.preview_version(package, stable, 144) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("run_number", [0, 65_536])
|
||||
def test_preview_rejects_wix_incompatible_run_number(run_number: int) -> None:
|
||||
with pytest.raises(ValueError, match="WiX"):
|
||||
MODULE.preview_version("0.5.2", "v0.5.1", run_number)
|
||||
|
||||
|
||||
def test_cli_rewrites_only_the_package_version(tmp_path: Path) -> None:
|
||||
package_json = tmp_path / "package.json"
|
||||
package_json.write_text(
|
||||
json.dumps({"name": "omnivoice-studio", "version": "0.5.2"}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--package-json",
|
||||
str(package_json),
|
||||
"--stable-tag",
|
||||
"v0.5.2",
|
||||
"--run-number",
|
||||
"7",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.stdout.strip() == "0.5.3-7"
|
||||
assert json.loads(package_json.read_text(encoding="utf-8")) == {
|
||||
"name": "omnivoice-studio",
|
||||
"version": "0.5.3-7",
|
||||
}
|
||||
|
||||
|
||||
def test_release_workflow_resolves_stable_once_before_the_matrix() -> None:
|
||||
workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
preview_gate = workflow.index(" preview-gate:")
|
||||
build_matrix = workflow.index(" build:", preview_gate)
|
||||
stable_lookup = workflow.index(
|
||||
"gh release view --repo \"$GITHUB_REPOSITORY\" --json tagName"
|
||||
)
|
||||
lookup_command = "gh release view --repo \"$GITHUB_REPOSITORY\" --json tagName"
|
||||
preview_gate_body = workflow[preview_gate:build_matrix]
|
||||
build_body = workflow[build_matrix:]
|
||||
assert preview_gate < stable_lookup < build_matrix
|
||||
assert workflow.count(lookup_command) == 1
|
||||
assert "STABLE_TAG=$(gh release view" in preview_gate_body
|
||||
assert '[[ "$STABLE_TAG" =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+$ ]]' in preview_gate_body
|
||||
assert 'echo "stable_tag=$STABLE_TAG" >> "$GITHUB_OUTPUT"' in preview_gate_body
|
||||
assert lookup_command not in build_body
|
||||
assert "stable_tag: ${{ steps.decide.outputs.stable_tag }}" in workflow
|
||||
assert "STABLE_TAG: ${{ needs.preview-gate.outputs.stable_tag }}" in build_body
|
||||
assert "python scripts/stamp-preview-version.py" in workflow
|
||||
assert '--stable-tag "$STABLE_TAG"' in workflow
|
||||
Reference in New Issue
Block a user