@@ -110,6 +110,8 @@ jobs:
110110 env :
111111 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
112112 PR_NUMBER : ${{ steps.pr.outputs.pr_number }}
113+ PR_BASE_SHA : ${{ steps.pr.outputs.base_sha }}
114+ PR_HEAD_SHA : ${{ steps.pr.outputs.head_sha }}
113115 CODEX_OUTPUT_PATH : ${{ steps.codex-output.outputs.output_file }}
114116 run : |
115117 python - <<'PY'
@@ -120,10 +122,119 @@ jobs:
120122 import re
121123
122124 pr_number = os.environ["PR_NUMBER"]
125+ pr_base_sha = os.environ.get("PR_BASE_SHA")
126+ pr_head_sha = os.environ.get("PR_HEAD_SHA")
123127 codex_output_path = pathlib.Path(os.environ["CODEX_OUTPUT_PATH"])
124128 changed_files_path = pathlib.Path(".tmp/pr-labels/changed-files.txt")
125129 changes_diff_path = pathlib.Path(".tmp/pr-labels/changes.diff")
126130
131+ def read_file_at(commit, path):
132+ if not commit:
133+ return None
134+ try:
135+ return subprocess.check_output(
136+ ["git", "show", f"{commit}:{path}"],
137+ text=True,
138+ )
139+ except subprocess.CalledProcessError:
140+ return None
141+
142+ def dependency_lines_for_pyproject(text: str) -> set[int]:
143+ dependency_lines: set[int] = set()
144+ current_section = None
145+ in_project_dependencies = False
146+
147+ for line_number, raw_line in enumerate(text.splitlines(), start=1):
148+ stripped = raw_line.strip()
149+ if stripped.startswith("[") and stripped.endswith("]"):
150+ if stripped.startswith("[[") and stripped.endswith("]]"):
151+ current_section = stripped[2:-2].strip()
152+ else:
153+ current_section = stripped[1:-1].strip()
154+ in_project_dependencies = False
155+ if current_section in ("project.optional-dependencies", "dependency-groups"):
156+ dependency_lines.add(line_number)
157+ continue
158+
159+ if current_section in ("project.optional-dependencies", "dependency-groups"):
160+ dependency_lines.add(line_number)
161+ continue
162+
163+ if current_section != "project":
164+ continue
165+
166+ if in_project_dependencies:
167+ dependency_lines.add(line_number)
168+ if "]" in stripped:
169+ in_project_dependencies = False
170+ continue
171+
172+ if stripped.startswith("dependencies") and "=" in stripped:
173+ dependency_lines.add(line_number)
174+ if "[" in stripped and "]" not in stripped:
175+ in_project_dependencies = True
176+
177+ return dependency_lines
178+
179+ def pyproject_dependency_changed(diff_text: str) -> bool:
180+ base_text = read_file_at(pr_base_sha, "pyproject.toml")
181+ head_text = read_file_at(pr_head_sha, "pyproject.toml")
182+ if base_text is None and head_text is None:
183+ return False
184+
185+ base_dependency_lines = (
186+ dependency_lines_for_pyproject(base_text) if base_text else set()
187+ )
188+ head_dependency_lines = (
189+ dependency_lines_for_pyproject(head_text) if head_text else set()
190+ )
191+
192+ in_pyproject = False
193+ base_line = None
194+ head_line = None
195+ hunk_re = re.compile(r"@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@")
196+
197+ for line in diff_text.splitlines():
198+ if line.startswith("+++ b/"):
199+ current_file = line[len("+++ b/") :].strip()
200+ in_pyproject = current_file == "pyproject.toml"
201+ base_line = None
202+ head_line = None
203+ continue
204+
205+ if not in_pyproject:
206+ continue
207+
208+ if line.startswith("@@ "):
209+ match = hunk_re.match(line)
210+ if not match:
211+ continue
212+ base_line = int(match.group(1))
213+ head_line = int(match.group(2))
214+ continue
215+
216+ if base_line is None or head_line is None:
217+ continue
218+
219+ if line.startswith(" "):
220+ base_line += 1
221+ head_line += 1
222+ continue
223+
224+ if line.startswith("-"):
225+ if base_line in base_dependency_lines:
226+ return True
227+ base_line += 1
228+ continue
229+
230+ if line.startswith("+"):
231+ if head_line in head_dependency_lines:
232+ return True
233+ head_line += 1
234+ continue
235+
236+ return False
237+
127238 changed_files = []
128239 if changed_files_path.exists():
129240 changed_files = [
@@ -137,22 +248,15 @@ jobs:
137248 desired.add("project")
138249 if any(path.startswith("docs/") for path in changed_files):
139250 desired.add("documentation")
140- if "uv.lock" in changed_files:
141- desired.add("dependencies")
251+ dependencies_allowed = "uv.lock" in changed_files
252+ diff_text = None
142253 if changes_diff_path.exists():
143254 diff_text = changes_diff_path.read_text()
144- current_file = None
145- for line in diff_text.splitlines():
146- if line.startswith("+++ b/"):
147- current_file = line[len("+++ b/") :].strip()
148- continue
149- if not current_file or not current_file.startswith(".github/workflows/"):
150- continue
151- if not line.startswith(("+", "-")):
152- continue
153- if re.search(r"uses:\s*(?!\./)(?!\.\/)[^@\s]+/[^@\s]+@", line):
154- desired.add("dependencies")
155- break
255+ if "pyproject.toml" in changed_files and pyproject_dependency_changed(diff_text):
256+ dependencies_allowed = True
257+
258+ if dependencies_allowed:
259+ desired.add("dependencies")
156260
157261 allowed = {
158262 "documentation",
@@ -184,6 +288,8 @@ jobs:
184288 pass
185289
186290 for label in codex_labels:
291+ if label == "dependencies" and not dependencies_allowed:
292+ continue
187293 if label in allowed:
188294 desired.add(label)
189295
0 commit comments