fix(upstream): use UTF-8 for framework version checks (#587)

* fix(upstream): use UTF-8 for framework version checks

* test(upstream): isolate argument and console codecs

* docs(changelog): record fix for #587

* changelog: place the #587 entry under [Unreleased]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-21 07:23:31 +02:00
1 parent 358d167cff
commit 6fcf0f045f
3 files changed
+55 -1

No files matched your search

+2
View File
@@ -428,6 +428,8 @@ per-file diff commands.
- **Upstream triage distinguishes subject-only matches from verified patch matches** (#569) - matching subjects with different patches now name the fork commit and request verification instead of claiming the change was already cherry-picked. - **Upstream triage distinguishes subject-only matches from verified patch matches** (#569) - matching subjects with different patches now name the fork commit and request verification instead of claiming the change was already cherry-picked.
- **Upstream version checks decode Git output as UTF-8** (#587) - future non-Latin framework lines and non-Latin branch names no longer depend on the host code page, guarding a latent case absent from the shipped framework files.
### Security ### Security
- **CI no longer leaves the job token in `.git/config` or expands a PR-controlled name into a - **CI no longer leaves the job token in `.git/config` or expands a PR-controlled name into a
+40
View File
@@ -1,3 +1,4 @@
import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
@@ -185,6 +186,45 @@ class UpstreamRefMissingFileTests(UpstreamCheckerRepoFixture):
self.assertIn("[WARNING]", result.stdout) self.assertIn("[WARNING]", result.stdout)
class LegacyEncodingTests(UpstreamCheckerRepoFixture):
def setUp(self):
super().setUp()
# Use the actual shipped framework files, whose UTF-8 prose git show
# returns, rather than a constructed string passed to run_git().
for rel in FRAMEWORK_FILES:
shutil.copy(REPO_ROOT / rel, self.root / rel)
subprocess.run(["git", "add", "-A"], cwd=self.root, check=True, capture_output=True)
subprocess.run(["git", "commit", "-qm", "copy actual framework files"], cwd=self.root, check=True, capture_output=True)
self.add_remote("upstream", TEMPLATE_URL)
self.materialize_remote_ref("upstream")
def run_legacy_checker(self, *args, ascii_git_codec=True):
env = dict(os.environ, LC_ALL="C", LANG="C", PYTHONUTF8="0",
PYTHONCOERCECLOCALE="0", PYTHONIOENCODING="cp1252")
if not ascii_git_codec:
# Keep valid Unicode argv while independently forcing legacy stdout.
# Linux's ASCII filesystem decoder would instead create surrogate
# escapes, testing argument decoding rather than console output.
env["PYTHONUTF8"] = "1"
return subprocess.run(
[sys.executable, str(self.root / "tools/check_upstream_updates.py"), "--no-fetch", *args],
cwd=self.root, env=env, capture_output=True,
)
def test_actual_framework_prose_is_decoded_as_utf8(self):
result = self.run_legacy_checker()
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace"))
self.assertIn("[OK]", result.stdout.decode("utf-8"))
def test_non_latin_branch_is_printed_as_utf8(self):
branch = "release-版本"
subprocess.run(["git", "update-ref", f"refs/remotes/upstream/{branch}", "HEAD"],
cwd=self.root, check=True, capture_output=True)
result = self.run_legacy_checker("--branch", branch, ascii_git_codec=False)
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace"))
self.assertIn(f"upstream/{branch}", result.stdout.decode("utf-8"))
class IncompleteComparisonTests(UpstreamCheckerRepoFixture): class IncompleteComparisonTests(UpstreamCheckerRepoFixture):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
+13 -1
View File
@@ -38,7 +38,10 @@ FRAMEWORK_FILES = [
UPSTREAM_REPO_SLUG = "MadsLorentzen/ai-job-search" UPSTREAM_REPO_SLUG = "MadsLorentzen/ai-job-search"
def run_git(args: list[str]) -> tuple[int, str, str]: def run_git(args: list[str]) -> tuple[int, str, str]:
res = subprocess.run(["git"] + args, cwd=str(ROOT), capture_output=True, text=True) res = subprocess.run(
["git"] + args, cwd=str(ROOT), capture_output=True, text=True,
encoding="utf-8", errors="replace",
)
return res.returncode, res.stdout, res.stderr return res.returncode, res.stdout, res.stderr
def get_remote_url(remote_name: str) -> str: def get_remote_url(remote_name: str) -> str:
@@ -65,7 +68,16 @@ def parse_semver(version_str: str) -> tuple[int, int, int]:
return (0, 0, 0) return (0, 0, 0)
return tuple(int(x) for x in match.groups()) return tuple(int(x) for x in match.groups())
def _force_utf8_output() -> None:
"""Use UTF-8 for piped output even on legacy-console hosts."""
for stream in (sys.stdout, sys.stderr):
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure:
reconfigure(encoding="utf-8")
def main() -> int: def main() -> int:
_force_utf8_output()
parser = argparse.ArgumentParser(description="Check for framework updates upstream.") parser = argparse.ArgumentParser(description="Check for framework updates upstream.")
parser.add_argument("--remote", default="upstream", help="Name of the git remote for upstream (default: upstream)") parser.add_argument("--remote", default="upstream", help="Name of the git remote for upstream (default: upstream)")
parser.add_argument("--branch", default="master", help="Branch name of the upstream repo (default: master)") parser.add_argument("--branch", default="master", help="Branch name of the upstream repo (default: master)")