From 72bbe00529772c9d8045e252df8e16970b82a68f Mon Sep 17 00:00:00 2001 From: Oscar Madera <80536682+oscarbol09@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:00:49 -0500 Subject: [PATCH] fix(check_upstream_updates): warn when check falls back to a fork's own origin (#265) On a fork without an 'upstream' remote, the checker silently fell back to 'origin' (the fork itself) and still printed '[OK] All framework files are up to date with upstream!', a false positive: the fork is always up to date with itself, so upstream updates were never reported. This is exactly the setup CONTRIBUTING.md recommends for forks. Now, when the fallback remote does not point at the ai-job-search template repo, the script warns that the comparison is fork-vs-self and prints the command to add the template as a remote. The final OK line now names the ref it actually compared against. Tests (new tests/test_check_upstream_updates.py, three scenarios) fail on master and pass with the fix. --- tests/test_check_upstream_updates.py | 119 +++++++++++++++++++++++++++ tools/check_upstream_updates.py | 20 ++++- 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/test_check_upstream_updates.py diff --git a/tests/test_check_upstream_updates.py b/tests/test_check_upstream_updates.py new file mode 100644 index 0000000..6eb49a0 --- /dev/null +++ b/tests/test_check_upstream_updates.py @@ -0,0 +1,119 @@ +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPT = REPO_ROOT / "tools" / "check_upstream_updates.py" + +TEMPLATE_URL = "https://github.com/MadsLorentzen/ai-job-search.git" +FORK_URL = "https://github.com/octocat/ai-job-search.git" + +FRAMEWORK_FILES = [ + ".claude/skills/job-application-assistant/01-candidate-profile.md", + ".claude/skills/job-application-assistant/02-behavioral-profile.md", + ".claude/skills/job-application-assistant/03-writing-style.md", + ".claude/skills/job-application-assistant/04-job-evaluation.md", + ".claude/skills/job-application-assistant/05-cv-templates.md", + ".claude/skills/job-application-assistant/06-cover-letter-templates.md", + ".claude/skills/job-application-assistant/07-interview-prep.md", + ".claude/skills/job-application-assistant/08-application-forms.md", + ".claude/skills/job-application-assistant/SKILL.md", + "AGENTS.md", +] + +FRONTMATTER = "---\nframework_version: 1.0.0\n---\n" + + +class UpstreamCheckerRepoFixture(unittest.TestCase): + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) + + tools = self.root / "tools" + tools.mkdir() + shutil.copy(SCRIPT, tools / "check_upstream_updates.py") + + for rel in FRAMEWORK_FILES: + path = self.root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(FRONTMATTER, encoding="utf-8") + + subprocess.run(["git", "init", "-b", "master"], cwd=self.root, check=True, capture_output=True) + subprocess.run(["git", "config", "user.name", "Test"], cwd=self.root, check=True, capture_output=True) + subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=self.root, check=True, capture_output=True) + subprocess.run(["git", "add", "-A"], cwd=self.root, check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "init"], cwd=self.root, check=True, capture_output=True) + + def add_remote(self, name: str, url: str) -> None: + subprocess.run(["git", "remote", "add", name, url], cwd=self.root, check=True, capture_output=True) + + def materialize_remote_ref(self, name: str) -> None: + subprocess.run( + ["git", "update-ref", f"refs/remotes/{name}/master", "HEAD"], + cwd=self.root, + check=True, + capture_output=True, + ) + + def run_checker(self, *args) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(self.root / "tools" / "check_upstream_updates.py"), "--no-fetch", *args], + cwd=self.root, + capture_output=True, + text=True, + ) + + +class ForkWithoutUpstreamRemoteTests(UpstreamCheckerRepoFixture): + def setUp(self): + super().setUp() + self.add_remote("origin", FORK_URL) + self.materialize_remote_ref("origin") + + def test_fork_fallback_warns_that_check_is_against_own_fork(self): + result = self.run_checker("--remote", "upstream") + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("Falling back to 'origin'", result.stdout) + self.assertIn("does not point to the ai-job-search template repo", result.stdout) + self.assertNotIn("up to date with upstream!", result.stdout) + self.assertIn("up to date with origin/master", result.stdout) + + +class DirectCloneFallbackTests(UpstreamCheckerRepoFixture): + def setUp(self): + super().setUp() + self.add_remote("origin", TEMPLATE_URL) + self.materialize_remote_ref("origin") + + def test_clone_of_template_falls_back_without_fork_warning(self): + result = self.run_checker("--remote", "upstream") + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("Falling back to 'origin'", result.stdout) + self.assertNotIn("does not point to the ai-job-search template repo", result.stdout) + self.assertIn("up to date with origin/master", result.stdout) + + +class UpstreamRemotePresentTests(UpstreamCheckerRepoFixture): + def setUp(self): + super().setUp() + self.add_remote("origin", FORK_URL) + self.add_remote("upstream", TEMPLATE_URL) + self.materialize_remote_ref("upstream") + + def test_explicit_upstream_remote_is_used_without_warning(self): + result = self.run_checker() + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("Falling back to 'origin'", result.stdout) + self.assertNotIn("does not point to the ai-job-search template repo", result.stdout) + self.assertIn("up to date with upstream/master", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/check_upstream_updates.py b/tools/check_upstream_updates.py index 3512941..f431fcb 100755 --- a/tools/check_upstream_updates.py +++ b/tools/check_upstream_updates.py @@ -33,10 +33,16 @@ FRAMEWORK_FILES = [ "AGENTS.md", ] +UPSTREAM_REPO_SLUG = "MadsLorentzen/ai-job-search" + def run_git(args: list[str]) -> tuple[int, str, str]: res = subprocess.run(["git"] + args, cwd=str(ROOT), capture_output=True, text=True) return res.returncode, res.stdout, res.stderr +def get_remote_url(remote_name: str) -> str: + rc, stdout, _ = run_git(["remote", "get-url", remote_name]) + return stdout.strip() if rc == 0 else "" + def get_framework_version_from_text(text: str) -> str | None: if not text.startswith("---\n"): return None @@ -76,6 +82,18 @@ def main() -> int: print("Error: No git remotes found.") return 1 + # A fork's own 'origin' can never reveal upstream updates: warn so the + # user is not misled by the final '[OK]' line below. (Direct clones of + # the template repo have origin == the upstream repo, so no warning.) + if remote != args.remote and UPSTREAM_REPO_SLUG not in get_remote_url(remote): + print( + f"Warning: Remote '{remote}' does not point to the ai-job-search " + f"template repo ({UPSTREAM_REPO_SLUG}), so this check compares your " + f"fork against itself and will never report upstream updates. " + f"Add the template repo as a remote to track upstream changes, e.g.:\n" + f" git remote add upstream https://github.com/{UPSTREAM_REPO_SLUG}.git" + ) + if not args.no_fetch: print(f"Fetching latest from remote '{remote}'...") rc, _, stderr = run_git(["fetch", remote]) @@ -143,7 +161,7 @@ def main() -> int: print("Review these changes to see if they fit your personalized fork!") return 0 else: - print("[OK] All framework files are up to date with upstream!") + print(f"[OK] All framework files are up to date with {ref}!") return 0 if __name__ == "__main__":