Compare commits

...
4 Commits
Author SHA1 Message Date
thePR0M3TH3AN 004cce2a4f chore: release v0.3.11 2026-03-19 22:05:16 -04:00
thePR0M3TH3AN ec0abd856c chore: release v0.3.10 2026-03-19 21:45:02 -04:00
thePR0M3TH3AN 7724ac78a7 chore: release v0.3.9 2026-03-19 21:36:04 -04:00
thePR0M3TH3AN c1a5e608de chore: release v0.3.8 2026-03-19 16:53:04 -04:00
12 changed files with 277 additions and 8 deletions

No files matched your search

+8
View File
@@ -70,6 +70,7 @@ jobs:
--add-data "voxvera/src${SEP}voxvera/src" \
--add-data "voxvera/templates${SEP}voxvera/templates" \
--add-data "voxvera/resources/tor${SEP}voxvera/resources/tor" \
--add-data "support-matrix.json${SEP}." \
--add-data "requirements.txt${SEP}." \
--name "${{ matrix.artifact_name }}" \
voxvera/cli.py
@@ -106,6 +107,13 @@ jobs:
chmod +x packaging/build_appimage.sh
./packaging/build_appimage.sh dist/${{ matrix.artifact_name }}
- name: Validate Linux release artifacts
if: matrix.os == 'ubuntu-22.04'
shell: bash
run: |
chmod +x scripts/validate-release-artifacts.sh
./scripts/validate-release-artifacts.sh
- name: Build Flatpak (Linux)
if: matrix.os == 'ubuntu-22.04'
run: |
+4
View File
@@ -106,6 +106,10 @@ When you host a flyer, the **"Download Source Code"** button on the landing page
- Installation and setup scripts for all platforms.
- Complete documentation and templates.
To keep the mirrored download practical for low-bandwidth redistribution, the
source archive does not ship pre-downloaded Tor binaries. Normal installer and
release builds still fetch or bundle those artifacts separately.
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
## Export & Backup
+4
View File
@@ -106,6 +106,10 @@ When you host a flyer, the **"הורד כלי וקוד מקור"** button on the
- Installation and setup scripts for all platforms.
- Complete documentation and templates.
To keep the mirrored download practical for low-bandwidth redistribution, the
source archive does not ship pre-downloaded Tor binaries. Normal installer and
release builds still fetch or bundle those artifacts separately.
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
## Export & Backup
+4
View File
@@ -106,6 +106,10 @@ When you host a flyer, the **"{{t('web.download_button')}}"** button on the land
- Installation and setup scripts for all platforms.
- Complete documentation and templates.
To keep the mirrored download practical for low-bandwidth redistribution, the
source archive does not ship pre-downloaded Tor binaries. Normal installer and
release builds still fetch or bundle those artifacts separately.
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
## Export & Backup
+1 -1
View File
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
[project]
name = "voxvera"
version = "0.3.7"
version = "0.3.11"
description = "VoxVera CLI utilities"
readme = "README.md"
authors = [{name = "VoxVera"}]
+1
View File
@@ -45,6 +45,7 @@ $PY_CMD --onefile \
--add-data "voxvera/src:voxvera/src" \
--add-data "voxvera/templates:voxvera/templates" \
--add-data "voxvera/resources/tor:voxvera/resources/tor" \
--add-data "support-matrix.json:." \
--add-data "requirements.txt:." \
--name "$BINARY_NAME" \
--distpath "$ARTIFACT_DIR" \
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
ARCH="${ARCH_OVERRIDE:-$(uname -m)}"
if [[ "$ARCH" == "x86_64" ]]; then
DEB_ARCH="amd64"
else
DEB_ARCH="$ARCH"
fi
DEB_PATH="${1:-voxvera/resources/bin/voxvera_$(python3 - <<'PY'
from voxvera import __version__
print(__version__)
PY
)_${DEB_ARCH}.deb}"
APPIMAGE_PATH="${2:-voxvera/resources/bin/VoxVera-${ARCH}.AppImage}"
tmpdir="$(mktemp -d)"
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT
json_get() {
local file="$1"
local key="$2"
python3 - "$file" "$key" <<'PY'
import json
import sys
path = sys.argv[2].split(".")
data = json.load(open(sys.argv[1], "r", encoding="utf-8"))
for part in path:
data = data[part]
if isinstance(data, list):
print("\n".join(str(item) for item in data))
else:
print(data)
PY
}
require_non_empty_json_field() {
local file="$1"
local key="$2"
local value
value="$(json_get "$file" "$key")"
if [[ -z "${value// }" ]]; then
echo "Expected non-empty JSON field '$key' in $file" >&2
exit 1
fi
}
echo "Validating Debian artifact: $DEB_PATH"
if [[ ! -f "$DEB_PATH" ]]; then
echo "Debian package not found: $DEB_PATH" >&2
exit 1
fi
mkdir -p "$tmpdir/deb-root" "$tmpdir/deb-data"
dpkg-deb -x "$DEB_PATH" "$tmpdir/deb-root"
if [[ ! -x "$tmpdir/deb-root/usr/bin/voxvera" ]]; then
echo "Packaged voxvera binary missing from .deb payload" >&2
exit 1
fi
VOXVERA_DIR="$tmpdir/deb-data" \
"$tmpdir/deb-root/usr/bin/voxvera" \
--config "$tmpdir/deb-data/config.json" \
init --non-interactive
if [[ ! -f "$tmpdir/deb-data/config.json" ]]; then
echo ".deb validation failed: config.json was not created" >&2
exit 1
fi
if [[ ! -f "$tmpdir/deb-data/host/voxvera/index.html" ]]; then
echo ".deb validation failed: built site missing index.html" >&2
exit 1
fi
echo "Validating AppImage artifact: $APPIMAGE_PATH"
if [[ ! -f "$APPIMAGE_PATH" ]]; then
echo "AppImage not found: $APPIMAGE_PATH" >&2
exit 1
fi
chmod +x "$APPIMAGE_PATH"
"$APPIMAGE_PATH" platform-status --json > "$tmpdir/platform-status.json"
require_non_empty_json_field "$tmpdir/platform-status.json" "tier_description"
require_non_empty_json_field "$tmpdir/platform-status.json" "hosting_model"
VOXVERA_DIR="$tmpdir/appimage-data" \
"$APPIMAGE_PATH" \
--config "$tmpdir/appimage-data/config.json" \
init --non-interactive
if [[ ! -f "$tmpdir/appimage-data/config.json" ]]; then
echo "AppImage validation failed: config.json was not created" >&2
exit 1
fi
if [[ ! -f "$tmpdir/appimage-data/host/voxvera/index.html" ]]; then
echo "AppImage validation failed: built site missing index.html" >&2
exit 1
fi
echo "Release artifact validation passed."
Binary file not shown.
+51
View File
@@ -81,6 +81,57 @@ def test_init_template(tmp_path, monkeypatch):
assert (dest / "index.html").exists()
def test_init_seeds_missing_default_config(tmp_path, monkeypatch):
_setup_tmp(monkeypatch, tmp_path)
test_data_dir = tmp_path / "data"
config_path = test_data_dir / "fresh-config.json"
answers = iter([
"Fresh Site",
"freshsite",
"Fresh Title",
"Fresh Subtitle",
"Fresh Headline",
"https://example.com",
"Read more",
"Footer bits",
"",
])
monkeypatch.setattr(cli, "choose_language", lambda *a: "en")
monkeypatch.setattr(cli, "prompt", lambda questions: {question["name"]: next(answers) for question in questions})
monkeypatch.setattr(cli, "open_editor", lambda initial: "Fresh content")
class _Confirm:
def execute(self):
return False
monkeypatch.setattr(cli.inquirer, "confirm", lambda **kwargs: _Confirm())
cli.main(["--config", str(config_path), "init"])
assert config_path.exists()
saved = cli.load_config(config_path)
assert saved["folder_name"] == "freshsite"
assert saved["content"] == "Fresh content"
def test_init_noninteractive_does_not_prompt_for_language(tmp_path, monkeypatch):
_setup_tmp(monkeypatch, tmp_path)
test_data_dir = tmp_path / "data"
config_path = test_data_dir / "fresh-config.json"
def _fail_choose_language(*args, **kwargs):
raise AssertionError("choose_language should not run for non-interactive init")
monkeypatch.setattr(cli, "choose_language", _fail_choose_language)
cli.main(["--config", str(config_path), "init", "--non-interactive"])
saved = cli.load_config(config_path)
assert saved["lang"] == "en"
assert (test_data_dir / "host" / saved["folder_name"] / "index.html").exists()
def test_create_from_template_persists_current_language(tmp_path, monkeypatch):
_setup_tmp(monkeypatch, tmp_path)
cli.load_locale("es")
+45
View File
@@ -56,6 +56,26 @@ def test_linux_validation_runbook_and_helper_are_linked():
assert "docs/linux-hosting-validation.md" in readme
assert "scripts/linux-recovery-check.sh" in runbook.read_text(encoding="utf-8")
def test_frozen_builds_bundle_support_matrix():
build_script = (REPO_ROOT / "scripts" / "build_binaries.sh").read_text(encoding="utf-8")
binaries_workflow = (REPO_ROOT / ".github" / "workflows" / "binaries.yml").read_text(encoding="utf-8")
assert '--add-data "support-matrix.json:."' in build_script
assert '--add-data "support-matrix.json${SEP}."' in binaries_workflow
def test_release_artifact_validation_is_wired_into_binaries_workflow():
validation_script = REPO_ROOT / "scripts" / "validate-release-artifacts.sh"
binaries_workflow = (REPO_ROOT / ".github" / "workflows" / "binaries.yml").read_text(encoding="utf-8")
script_text = validation_script.read_text(encoding="utf-8")
assert validation_script.exists()
assert "scripts/validate-release-artifacts.sh" in binaries_workflow
assert "dpkg-deb -x" in script_text
assert "platform-status --json" in script_text
assert "init --non-interactive" in script_text
def test_locale_completeness():
"""Verify that all non-English locales have the same keys as English."""
locales_dir = REPO_ROOT / "voxvera" / "locales"
@@ -210,3 +230,28 @@ def test_portable_bundle_integrity(tmp_path, monkeypatch):
# Verify root scripts
assert "voxvera-run.sh" in names
def test_portable_bundle_excludes_prebuilt_tor_binaries(tmp_path, monkeypatch):
project_root = tmp_path
voxvera_dir = project_root / "voxvera"
voxvera_dir.mkdir()
shutil.copytree(REPO_ROOT / "voxvera" / "src", voxvera_dir / "src")
shutil.copytree(REPO_ROOT / "voxvera" / "locales", voxvera_dir / "locales")
shutil.copytree(REPO_ROOT / "voxvera" / "resources", voxvera_dir / "resources")
os.makedirs(voxvera_dir / "vendor", exist_ok=True)
(voxvera_dir / "vendor" / "anchor.txt").write_text("anchor")
for script in ["voxvera-run.sh", "uninstall.sh", "uninstall.ps1", "install.sh", "install.ps1", "setup.sh"]:
if (REPO_ROOT / script).exists():
shutil.copy(REPO_ROOT / script, project_root / script)
monkeypatch.setattr(cli, "ROOT", voxvera_dir)
bundle_zip = project_root / "voxvera-portable.zip"
cli.bundle_portable(bundle_zip)
with zipfile.ZipFile(bundle_zip, "r") as zf:
names = zf.namelist()
assert not any(name.startswith("voxvera/resources/tor/") for name in names)
+1 -1
View File
@@ -1,7 +1,7 @@
import sys
from pathlib import Path
__version__ = "0.3.7"
__version__ = "0.3.11"
# Add the vendor directory to sys.path ONLY for PyInstaller (frozen) builds.
# For pip/pipx installs the system/venv packages are correct and the vendored
+48 -6
View File
@@ -341,6 +341,18 @@ def save_config(data: dict, path: str):
json.dump(normalize_config(data), fh, indent=2)
def ensure_config_exists(path: str | Path) -> Path:
"""Seed a config file from the bundled defaults when missing."""
config_path = Path(path)
if config_path.exists():
return config_path
config_path.parent.mkdir(parents=True, exist_ok=True)
default_config = json.loads((_src_res("config.json")).read_text(encoding="utf-8"))
config_path.write_text(json.dumps(normalize_config(default_config), indent=2), encoding="utf-8")
return config_path
def _open_editor_terminal(initial: str) -> str:
"""Fallback to opening the user's $EDITOR in the terminal."""
import tempfile
@@ -530,6 +542,7 @@ def resolve_new_site_folder(config_path: str, preserve_session_on_overwrite: boo
def interactive_update(config_path: str):
ensure_config_exists(config_path)
data = load_config(config_path)
console = Console()
@@ -680,12 +693,26 @@ def copy_template(name: str) -> str:
def bundle_source(dest_zip: Path):
"""Bundle the VoxVera source code, dependencies (vendor), and root scripts."""
"""Bundle the VoxVera source tree and install scripts for viral mirroring.
We intentionally exclude pre-downloaded Tor binaries from the mirrored source
archive. They dominate the zip size and are regenerated by the normal
packaging/build flows, while the mirrored source bundle is meant to stay
small enough for practical redistribution from flyer sites.
"""
with zipfile.ZipFile(dest_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 1. Include the voxvera/ source directory (excluding host, bin, pycache)
# We DO include vendor here so the source zip is fully functional offline.
# 1. Include the voxvera/ source directory (excluding host, bin,
# bundled Tor artifacts, and pycache). We still include vendor so the
# source zip remains portable without re-resolving Python deps.
for root, dirs, files in os.walk(ROOT):
if "__pycache__" in root or "host" in root or "resources/bin" in root:
root_path = Path(root)
if (
"__pycache__" in root
or "host" in root
or "resources/bin" in root
or root_path == ROOT / "resources" / "tor"
or (ROOT / "resources" / "tor") in root_path.parents
):
continue
for file in files:
file_path = Path(root) / file
@@ -2030,7 +2057,10 @@ def main(argv=None):
pass
if not current_lang and args.command in ("init", "quickstart", "manage"):
current_lang = choose_language()
if getattr(args, "non_interactive", False):
current_lang = "en"
else:
current_lang = choose_language()
# Save choice to config if possible
if config_path.exists():
config_data["lang"] = current_lang
@@ -2063,7 +2093,13 @@ def main(argv=None):
if args.template:
copy_template(args.template)
return
elif not args.non_interactive:
ensure_config_exists(config_path)
if current_lang:
config_data = load_config(str(config_path))
if not config_data.get("lang"):
config_data["lang"] = current_lang
save_config(config_data, str(config_path))
if not args.non_interactive:
interactive_update(config_path)
build_assets(config_path)
elif args.command == "build":
@@ -2094,6 +2130,12 @@ def main(argv=None):
else:
install_autostart()
elif args.command == "quickstart":
ensure_config_exists(config_path)
if current_lang:
config_data = load_config(str(config_path))
if not config_data.get("lang"):
config_data["lang"] = current_lang
save_config(config_data, str(config_path))
if not args.non_interactive:
if not sys.stdin.isatty():
print(