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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ body:
attributes:
label: Pearipherals version
description: Use the About/version information, release name or source commit. State whether the EXE is an unsigned prerelease.
placeholder: "1.2.0 unsigned prerelease"
placeholder: "Current unsigned prerelease"
validations:
required: true

Expand Down
102 changes: 102 additions & 0 deletions .github/ISSUE_TEMPLATE/compatibility_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Compatibility report
description: Tell us whether Pearipherals works on your Apple hardware and Windows setup
title: "[Compatibility]: "
labels:
- compatibility
body:
- type: markdown
attributes:
value: |
Working and failed setups are both useful. Please report product-level details only.

**Privacy:** do not include a serial number, Bluetooth address, HID identifier, username, full local path, registry export, configuration file, error log, or credential. Do not attach raw HID data unless a maintainer has explicitly arranged a private collection process.

Do not disable Windows security protections to test Pearipherals.

- type: input
id: pearipherals-version
attributes:
label: Pearipherals version
description: Copy the version from About / status or the GitHub release name.
placeholder: "1.2.1 unsigned prerelease"
validations:
required: true

- type: input
id: windows-version
attributes:
label: Windows version
description: Include Windows 10/11, release, and OS build. Do not include your device name.
placeholder: "Windows 11 24H2, OS build 26100.x"
validations:
required: true

- type: input
id: device-model
attributes:
label: Apple device model
description: List the product family and visible model revision only. Do not include any serial number or device identifier.
placeholder: "Magic Keyboard A2450 and Magic Trackpad 2 A1535"
validations:
required: true

- type: dropdown
id: connection
attributes:
label: Connection
options:
- Bluetooth
- USB data
- USB-C to DisplayPort video-only
- Mixed connections
- Other or unknown
validations:
required: true

- type: dropdown
id: overall-result
attributes:
label: Overall result
options:
- Works fully
- Works partly
- Does not work
validations:
required: true

- type: checkboxes
id: features-tested
attributes:
label: Features tested
description: Select every feature you actually tried.
options:
- label: Magic Keyboard F-row controls
- label: F6 region-selection overlay
- label: Magic Trackpad pointer and two-finger scrolling
- label: Three-finger swipes
- label: Three-finger drag
- label: Natural scrolling toggle
- label: Keyboard or trackpad battery display
- label: Display brightness
- label: Autostart, quit, and relaunch

- type: textarea
id: notes
attributes:
label: What worked or failed?
description: Keep this short. For a failure, give the smallest repeatable sequence and remove private information.
placeholder: "F-row, battery, and two-finger scrolling work. Three-finger swipe left does not open Task View after reconnecting Bluetooth."
validations:
required: true

- type: checkboxes
id: privacy-confirmation
attributes:
label: Privacy and safety confirmation
options:
- label: I removed every serial number, Bluetooth address, device identifier, username, full local path, registry value, and credential.
required: true
- label: I did not attach raw HID data, a full configuration, an error log, a registry export, or a crash dump.
required: true
- label: I did not disable or recommend disabling Windows security protections.
required: true
2 changes: 1 addition & 1 deletion .github/workflows/build-sign-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
version:
description: Release version (MAJOR.MINOR.PATCH)
required: true
default: 1.2.0
default: 1.2.1
submit_for_signing:
description: Submit this manual build to SignPath
required: true
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ trackpad driver.
Pearipherals is portable. Its configuration and optional error log sit beside
the EXE as `pearipherals.json` and `pearipherals.err.log`.

## Help test Pearipherals

Pearipherals needs reports from Apple hardware beyond the maintainer's setup.
If you try the beta, submit a short [compatibility report](../../issues/new?template=compatibility_report.yml),
including when everything works. A working setup is as useful as a failed setup
because it tells us which device revisions and connection paths are reliable.
The form asks only for product-level details and warns against sharing serial
numbers, Bluetooth addresses, raw HID data, or other private information.

## Windows security, unsigned builds, and Smart App Control

An **unsigned** prerelease has no Authenticode signature, so Windows cannot
Expand Down Expand Up @@ -201,7 +210,7 @@ Pearipherals first managed them.
```bat
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt
set PEARIPHERALS_VERSION=1.2.0
set PEARIPHERALS_VERSION=1.2.1
build.bat
```

Expand Down
4 changes: 2 additions & 2 deletions build.bat
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@echo off
rem Build Pearipherals.exe into dist\ — run from the repo root.
rem Set PEARIPHERALS_VERSION for a release build; local builds default to 1.2.0.
if not defined PEARIPHERALS_VERSION set "PEARIPHERALS_VERSION=1.2.0"
rem Set PEARIPHERALS_VERSION for a release build; local builds default to 1.2.1.
if not defined PEARIPHERALS_VERSION set "PEARIPHERALS_VERSION=1.2.1"
if not exist .venv (
python -m venv .venv || goto :err
.venv\Scripts\pip install -r requirements.txt || goto :err
Expand Down
2 changes: 1 addition & 1 deletion pearipherals_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass


APP_VERSION = "1.2.0"
APP_VERSION = "1.2.1"

# A build identity is always prefixed so the reader knows exactly what was
# measured. `git:` names the exact source revision a frozen build was produced
Expand Down
8 changes: 6 additions & 2 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3474,8 +3474,10 @@ def test_invalid_embedded_manifest_never_falls_back_to_adjacent_manifest(self):

identity = self._identity(exe_dir, meipass=meipass)()

from pearipherals_version import APP_VERSION

self.assertEqual("git:" + "0" * 40, identity.build_id)
self.assertEqual("1.2.0", identity.version)
self.assertEqual(APP_VERSION, identity.version)

def test_present_falsey_meipass_never_reads_adjacent_or_working_directory(self):
with tempfile.TemporaryDirectory() as exe_dir, \
Expand All @@ -3484,13 +3486,15 @@ def test_present_falsey_meipass_never_reads_adjacent_or_working_directory(self):
self._write_manifest(working_dir, "git:" + "f" * 40)
previous_directory = os.getcwd()
try:
from pearipherals_version import APP_VERSION

os.chdir(working_dir)
root_relative = os.path.splitdrive(working_dir)[1]
for meipass in ("", root_relative):
with self.subTest(meipass=meipass):
identity = self._identity(exe_dir, meipass=meipass)()
self.assertEqual("git:" + "0" * 40, identity.build_id)
self.assertEqual("1.2.0", identity.version)
self.assertEqual(APP_VERSION, identity.version)
finally:
os.chdir(previous_directory)

Expand Down
65 changes: 65 additions & 0 deletions tests/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
GITIGNORE = ROOT / ".gitignore"
PRIVACY_POLICY = ROOT / "PRIVACY.md"
README = ROOT / "README.md"
COMPATIBILITY_REPORT_FORM = ISSUE_TEMPLATE_DIR / "compatibility_report.yml"
TARGET_RELEASE_VERSION = "1.2.1"
SPEC = importlib.util.spec_from_file_location("write_version_info", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
Expand Down Expand Up @@ -62,6 +64,25 @@ def test_cli_writes_utf8_version_resource(self):
)
self.assertIn("ProductVersion', '1.2.3'", output.read_text(encoding="utf-8"))

def test_runtime_declares_target_release_version(self):
import pearipherals_version

self.assertEqual(TARGET_RELEASE_VERSION, pearipherals_version.APP_VERSION)

def test_local_and_hosted_build_defaults_match_runtime_version(self):
import pearipherals_version

build_script = BUILD_SCRIPT.read_text(encoding="utf-8")
workflow = WORKFLOW.read_text(encoding="utf-8")
self.assertIn(
f'if not defined PEARIPHERALS_VERSION set "PEARIPHERALS_VERSION={pearipherals_version.APP_VERSION}"',
build_script,
)
self.assertRegex(
workflow,
rf"(?m)^\s*default:\s*{re.escape(pearipherals_version.APP_VERSION)}\s*$",
)

def test_release_workflow_pins_actions_and_avoids_template_injection(self):
workflow = WORKFLOW.read_text(encoding="utf-8")
uses = re.findall(r"^\s*uses:\s*([^\s#]+)", workflow, flags=re.MULTILINE)
Expand Down Expand Up @@ -289,6 +310,12 @@ def test_bug_form_collects_compatibility_context_without_private_data(self):
with self.subTest(warning=warning):
self.assertIn(warning, form)

def test_bug_form_uses_a_version_neutral_release_example(self):
form = BUG_REPORT_FORM.read_text(encoding="utf-8").lower()

self.assertIn('placeholder: "current unsigned prerelease"', form)
self.assertNotIn("1.2.0 unsigned prerelease", form)

def test_feature_form_captures_problem_scope_alternatives_and_safety(self):
self.assertTrue(
FEATURE_REQUEST_FORM.is_file(),
Expand All @@ -310,6 +337,33 @@ def test_feature_form_captures_problem_scope_alternatives_and_safety(self):
self.assertIn("privacy", form)
self.assertIn("remove sensitive information", form)

def test_compatibility_form_collects_success_and_failure_reports_safely(self):
self.assertTrue(
COMPATIBILITY_REPORT_FORM.is_file(),
f"missing compatibility form: {COMPATIBILITY_REPORT_FORM}",
)
form = COMPATIBILITY_REPORT_FORM.read_text(encoding="utf-8").lower()
for field_id in (
"pearipherals-version",
"windows-version",
"device-model",
"connection",
"overall-result",
"features-tested",
"privacy-confirmation",
):
with self.subTest(field_id=field_id):
self.assertIn(f"id: {field_id}", form)
for result in ("works fully", "works partly", "does not work"):
self.assertIn(result, form)
for warning in (
"serial number",
"bluetooth address",
"raw hid",
"do not disable windows security",
):
self.assertIn(warning, form)

def test_issue_chooser_disables_blank_issues_and_links_private_security(self):
self.assertTrue(ISSUE_CONFIG.is_file(), f"missing issue config: {ISSUE_CONFIG}")
config = ISSUE_CONFIG.read_text(encoding="utf-8").lower()
Expand Down Expand Up @@ -344,6 +398,17 @@ def test_dependabot_updates_only_github_actions_on_a_conservative_schedule(self)


class SupportDocumentationTests(unittest.TestCase):
def test_readme_invites_beta_compatibility_reports(self):
readme = README.read_text(encoding="utf-8")

self.assertIn("## Help test Pearipherals", readme)
self.assertIn(
"../../issues/new?template=compatibility_report.yml",
readme,
)
self.assertIn("working setup", readme.lower())
self.assertIn("failed setup", readme.lower())

def test_readme_documents_the_complete_support_and_lifecycle_surface(self):
readme = README.read_text(encoding="utf-8")
lowered = readme.lower()
Expand Down
Loading