Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"

research-edition-checks:
name: Research Edition — patch table smoke test
name: Research Edition — patch and packaging smoke tests
# Run on every PR and push so drifted submodule pins fail at review time,
# not when Erik pushes the annotated release tag. The patcher is stdlib-only
# so no poetry/build step is needed; only submodules (real source files) and
Expand Down Expand Up @@ -286,8 +286,10 @@ jobs:

# Fixture-based unit tests: patchers apply correctly on synthetic trees,
# fail closed on missing/ambiguous tokens, and leave ordinary defaults
# untouched on the unpatched tree.
- name: Run profile patcher unit tests
# untouched on the unpatched tree. Also exercise the Windows installer
# collection step with fixture .exe files named by the real patched .iss
# sources; this catches naming drift without Inno Setup or signing.
- name: Run profile patcher and installer collection tests
run: python3 -m pytest scripts/tests/test_patch_research_edition_profile.py -q

build-qt:
Expand Down
14 changes: 14 additions & 0 deletions scripts/package/collect-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

set -e

# The Research Edition patcher rewrites OutputBaseFilename in the .iss
# (activitywatch-research[-tauri]-setup), so resolve the produced file
# instead of hardcoding the standard name. dist/ is fresh at this point,
# so exactly one setup exe must exist.
setup_src=(dist/activitywatch*-setup.exe)
if [ ${#setup_src[@]} -ne 1 ] || [ ! -f "${setup_src[0]}" ]; then
echo "ERROR: expected exactly one dist/activitywatch*-setup.exe, got: ${setup_src[*]}"
exit 1
fi
mv "${setup_src[0]}" "dist/$1"
12 changes: 1 addition & 11 deletions scripts/package/package-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,7 @@ function build_setup() {
else
env AW_VERSION=$version_no_prefix "$innosetupdir/iscc.exe" scripts/package/activitywatch-setup.iss
fi
# The Research Edition patcher rewrites OutputBaseFilename in the .iss
# (activitywatch-research[-tauri]-setup), so resolve the produced file
# instead of hardcoding the standard name. dist/ is fresh at this point,
# so exactly one setup exe must exist.
setup_src=(dist/activitywatch*-setup.exe)
if [ ${#setup_src[@]} -ne 1 ] || [ ! -f "${setup_src[0]}" ]; then
echo "ERROR: expected exactly one dist/activitywatch*-setup.exe, got: ${setup_src[*]}"
exit 1
fi
mv "${setup_src[0]}" "dist/$filename"
"$SCRIPT_DIR/collect-setup.sh" "$filename"
echo "Setup built!"
}

Expand All @@ -130,4 +121,3 @@ echo "-------------------------------------"
echo "Contents of ./dist"
ls -l dist
echo "-------------------------------------"

131 changes: 131 additions & 0 deletions scripts/tests/test_patch_research_edition_profile.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import fnmatch
import importlib.util
import os
import re
import subprocess
import sys
from pathlib import Path
from string import Template

import pytest

Expand Down Expand Up @@ -335,6 +339,133 @@ def _output_name(text: str) -> str:
assert "DefaultDirName={autopf}\\ActivityWatch-Tauri\n" not in tauri


@pytest.fixture(
params=[(target, research) for target in ("qt", "tauri") for research in (False, True)]
)
def windows_installer_names(tmp_path: Path, request):
target, research = request.param
rel = WINDOWS_REAL_FILES[target]
# Packaging inputs are tracked root files, so a missing producer must fail.
assert (_repo_root() / rel).is_file()
patches = (
patcher.WINDOWS_PATCHES_QT if target == "qt" else patcher.WINDOWS_PATCHES_TAURI
)
text = _patch_real_file(tmp_path, rel, patches if research else [])
outputs = re.findall(r"^OutputBaseFilename=(\S+)$", text, re.MULTILINE)
assert len(outputs) == 1, "expected one executable OutputBaseFilename assignment"

package = (_repo_root() / "scripts/package/package-all.sh").read_text()
calls = re.findall(
r'^\s*"\$SCRIPT_DIR/collect-setup\.sh" "\$filename"\s*$', package, re.MULTILINE
)
assert len(calls) == 1, "build_setup must invoke the tested installer collector"
templates = re.findall(
r'^\s*filename="([^"\n]+-setup\.exe)"$', package, re.MULTILINE
)
assert len(templates) == 1, "expected one versioned installer filename assignment"
suffix = "-tauri" if target == "tauri" else ""
if research:
suffix += "-research"
final = Template(templates[0]).substitute(
build_suffix=suffix, version="v0.14.0b5", platform="windows", arch="x86_64"
)
return target, outputs[0] + ".exe", final


def test_windows_installer_producer_matches_packaging_and_release(windows_installer_names):
target, produced, final = windows_installer_names
collector = (_repo_root() / "scripts/package/collect-setup.sh").read_text()
inputs = re.findall(r"^\s*setup_src=\(([^)\n]+)\)$", collector, re.MULTILINE)
assert len(inputs) == 1, "expected one executable setup_src assignment"
assert fnmatch.fnmatchcase(f"dist/{produced}", inputs[0].strip())

workflow = (_repo_root() / ".github/workflows/release.yml").read_text()
jobs = dict(re.findall(
r"^ ([\w-]+):\n(.*?)(?=^ [\w-]+:|\Z)", workflow, re.MULTILINE | re.DOTALL
))
upload = re.search(
r"^ - name: Upload packages\n(.*?)(?=^ - |\Z)",
jobs[f"build-{target}"], re.MULTILINE | re.DOTALL,
)
assert upload, f"missing {target} package upload step"
assert "uses: actions/upload-artifact@" in upload[1]
path = re.search(r"^ path: (.+)$", upload[1], re.MULTILINE)
assert path, f"missing {target} package upload path"
globs = (
re.findall(r"^ (\S+)$", upload[1], re.MULTILINE)
if path[1] == "|" else [path[1]]
)
assert globs and any(fnmatch.fnmatchcase(f"dist/{final}", glob) for glob in globs)
release = re.search(
r"^ - name: Release\n(.*?)(?=^ - |\Z)",
jobs["release"], re.MULTILINE | re.DOTALL,
)
assert release and "uses: softprops/action-gh-release@" in release[1]
files = re.search(
r"^ files: \|\n((?:^ \S.*\n)+)", release[1], re.MULTILINE
)
assert files, "missing release action's asset files block"
release_globs = [line.strip() for line in files[1].splitlines()]
Comment thread
TimeToBuildBob marked this conversation as resolved.
assert release_globs and any(
fnmatch.fnmatchcase(f"dist/builds-windows/{final}", glob) for glob in release_globs
)


@pytest.mark.skipif(
sys.platform != "linux", reason="Bash fixture runs in the Ubuntu packaging smoke job"
)
def test_windows_setup_collection(tmp_path: Path, windows_installer_names):
_, produced, final = windows_installer_names
dist = tmp_path / "dist"
dist.mkdir()
(dist / produced).write_bytes(b"fixture installer")
(dist / "activitywatch-portable.zip").write_bytes(b"portable archive")

result = subprocess.run(
["bash", str(_repo_root() / "scripts/package/collect-setup.sh"), final],
cwd=tmp_path, capture_output=True, text=True,
)

assert result.returncode == 0, result.stdout + result.stderr
assert {path.name for path in dist.iterdir()} == {final, "activitywatch-portable.zip"}
assert (dist / final).read_bytes() == b"fixture installer"
assert (dist / "activitywatch-portable.zip").read_bytes() == b"portable archive"


@pytest.mark.skipif(
sys.platform != "linux", reason="Bash fixture runs in the Ubuntu packaging smoke job"
)
@pytest.mark.parametrize("case", ["missing", "multiple", "directory"])
def test_windows_setup_collection_refuses_invalid_inputs(tmp_path: Path, case: str):
dist = tmp_path / "dist"
dist.mkdir()
(dist / "activitywatch-portable.zip").write_bytes(b"portable archive")
if case == "multiple":
for name in ("activitywatch-setup.exe", "activitywatch-research-setup.exe"):
(dist / name).write_bytes(name.encode())
elif case == "directory":
(dist / "activitywatch-research-setup.exe").mkdir()

def snapshot():
return {
path.name: path.read_bytes() if path.is_file() else None
for path in dist.iterdir()
}

before = snapshot()
result = subprocess.run(
[
"bash", str(_repo_root() / "scripts/package/collect-setup.sh"),
"activitywatch-research-v0.14.0b5-windows-x86_64-setup.exe",
],
cwd=tmp_path, capture_output=True, text=True,
)

assert result.returncode != 0
assert "expected exactly one" in result.stdout + result.stderr
assert snapshot() == before


def test_tauri_wix_upgrade_code_is_pinned_and_config_stays_valid_json(tmp_path: Path):
"""The MSI upgrade code defines the product family; pin it, don't derive it."""
import json
Expand Down