claude-dev-env 8.46.14 → 8.46.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ rather than the OS temp directory, matching the sibling workflow-hook tests.
12
12
  import contextlib
13
13
  import functools
14
14
  import importlib.util
15
+ import inspect
15
16
  import json
16
17
  import os
17
18
  import shutil
@@ -59,9 +60,15 @@ def _strip_read_only_and_retry(removal_function, target_path, *_exc_info):
59
60
  pass
60
61
 
61
62
 
63
+ _rmtree_supports_onexc = "onexc" in inspect.signature(shutil.rmtree).parameters
64
+
65
+
62
66
  def _force_rmtree(target_path: str) -> None:
63
67
  with contextlib.suppress(OSError):
64
- shutil.rmtree(target_path, onexc=_strip_read_only_and_retry)
68
+ if _rmtree_supports_onexc:
69
+ shutil.rmtree(target_path, onexc=_strip_read_only_and_retry)
70
+ else:
71
+ shutil.rmtree(target_path, onerror=_strip_read_only_and_retry)
65
72
 
66
73
 
67
74
  @functools.lru_cache(maxsize=1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.46.14",
3
+ "version": "8.46.16",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -82,6 +82,7 @@ from dev_env_scripts_constants.agent_merge_check_constants import (
82
82
  JSON_CONTENT_TYPE,
83
83
  MERGE_EXIT_CODE,
84
84
  MERGE_QUEUE_EJECTION_HOLD_TEMPLATE,
85
+ MERGE_QUEUE_GRAPHQL_FAILURE_TEMPLATE,
85
86
  MERGE_QUEUE_REMOVAL_PAGE_SIZE,
86
87
  MERGE_QUEUE_REMOVAL_QUERY,
87
88
  MERGE_QUEUE_RULE_TYPE,
@@ -877,36 +878,60 @@ def _field_at(document: object, all_fields: object, key: str) -> object:
877
878
  return all_fields.get(key)
878
879
 
879
880
 
880
- def read_merge_queue_ejection(slug: str, number: int, token: str) -> bool:
881
+ def read_merge_queue_ejection(
882
+ slug: str, number: int, base_ref: str, token: str
883
+ ) -> bool:
881
884
  """Read whether the merge queue ejected the pull request's current head.
882
885
 
883
- Only GraphQL reports why a pull request left the merge queue. A
884
- repository with no merge queue reports no removal events.
886
+ Only GraphQL reports a merge queue removal, and a Claude Code session's
887
+ proxy refuses GraphQL. So the base branch rules come first, over REST.
888
+ Rules that show no merge queue skip the GraphQL query.
885
889
 
886
890
  Args:
887
891
  slug: The repository as ``owner/name``.
888
892
  number: The pull request number.
893
+ base_ref: The branch the pull request merges into.
889
894
  token: The GitHub token the request authenticates with.
890
895
 
891
896
  Returns:
892
897
  True when a ``failed_checks`` removal is newer than the head commit.
898
+ False when the base branch rules show no merge queue.
893
899
 
894
900
  Raises:
895
- MergeCheckError: The query failed, or answered with another shape.
901
+ MergeCheckError: A read failed, or answered with another shape. A
902
+ refused GraphQL query names the merge queue that needed it.
896
903
  """
897
- document = _request_json(
898
- GITHUB_GRAPHQL_ENDPOINT,
899
- token,
900
- _pull_request_query_payload(
901
- MERGE_QUEUE_REMOVAL_QUERY, MERGE_QUEUE_REMOVAL_PAGE_SIZE, slug, number
902
- ),
903
- )
904
+ if not _may_merge_through_a_queue(slug, base_ref, token):
905
+ return False
906
+ document = _merge_queue_removal_document(slug, number, base_ref, token)
904
907
  return _is_ejected_on_head(
905
908
  _list_along(document, ALL_MERGE_QUEUE_REMOVAL_NODE_KEYS),
906
909
  _head_committed_at(document),
907
910
  )
908
911
 
909
912
 
913
+ def _merge_queue_removal_document(
914
+ slug: str, number: int, base_ref: str, token: str
915
+ ) -> object:
916
+ payload = _pull_request_query_payload(
917
+ MERGE_QUEUE_REMOVAL_QUERY, MERGE_QUEUE_REMOVAL_PAGE_SIZE, slug, number
918
+ )
919
+ try:
920
+ return _request_json(GITHUB_GRAPHQL_ENDPOINT, token, payload)
921
+ except MergeCheckError as failure:
922
+ raise MergeCheckError(
923
+ MERGE_QUEUE_GRAPHQL_FAILURE_TEMPLATE.format(base=base_ref, failure=failure)
924
+ ) from failure
925
+
926
+
927
+ def _may_merge_through_a_queue(slug: str, base_ref: str, token: str) -> bool:
928
+ try:
929
+ all_rules = _read_branch_rules(slug, urllib.parse.quote(base_ref), token)
930
+ except MergeCheckError:
931
+ return True
932
+ return has_merge_queue_rule(all_rules)
933
+
934
+
910
935
  def _list_along(document: object, all_keys: Sequence[str]) -> list[object]:
911
936
  all_records = functools.reduce(
912
937
  functools.partial(_field_at, document), all_keys, document
@@ -985,7 +1010,10 @@ def main(all_arguments: Sequence[str]) -> int:
985
1010
  parsed.slug, parsed.number, token
986
1011
  )
987
1012
  is_ejected_from_merge_queue = read_merge_queue_ejection(
988
- parsed.slug, parsed.number, token
1013
+ parsed.slug,
1014
+ parsed.number,
1015
+ str(_nested_field(all_pull_request_fields, BASE_KEY, REF_KEY)),
1016
+ token,
989
1017
  )
990
1018
  except MergeCheckError as failure:
991
1019
  print(failure, file=sys.stderr)
@@ -142,6 +142,10 @@ HOLD_EXIT_CODE = 1
142
142
  ERROR_EXIT_CODE = 2
143
143
 
144
144
  NO_SIGN_IN_MESSAGE = "No GitHub token in the environment. Set GH_TOKEN or GITHUB_TOKEN."
145
+ MERGE_QUEUE_GRAPHQL_FAILURE_TEMPLATE = (
146
+ "The {base} branch merges through a merge queue, and only the GitHub GraphQL"
147
+ " API reports a merge queue ejection. That GraphQL read failed: {failure}"
148
+ )
145
149
  SLUG_SEPARATOR = "/"
146
150
  SLUG_ARGUMENT_HELP = "Repository as owner/name, such as jl-cmd/claude-dev-env."
147
151
  NUMBER_ARGUMENT_HELP = "Pull request number."
@@ -29,6 +29,7 @@ CLEAN_PULL_REQUEST = {
29
29
  "number": 1442,
30
30
  "draft": False,
31
31
  "mergeable_state": "clean",
32
+ "base": {"ref": "main"},
32
33
  "head": {"sha": "ab845eb6945650055ebe852312a9057b9f067d6a"},
33
34
  }
34
35
 
@@ -293,7 +294,7 @@ def test_a_ready_pull_request_exits_zero(monkeypatch: pytest.MonkeyPatch) -> Non
293
294
  monkeypatch.setattr(
294
295
  agent_merge_check,
295
296
  "read_merge_queue_ejection",
296
- lambda slug, number, token: False,
297
+ lambda slug, number, base_ref, token: False,
297
298
  )
298
299
  assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 0
299
300
 
@@ -313,7 +314,7 @@ def test_a_held_pull_request_exits_one(monkeypatch: pytest.MonkeyPatch) -> None:
313
314
  monkeypatch.setattr(
314
315
  agent_merge_check,
315
316
  "read_merge_queue_ejection",
316
- lambda slug, number, token: False,
317
+ lambda slug, number, base_ref, token: False,
317
318
  )
318
319
  assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 1
319
320
 
@@ -655,12 +656,15 @@ def _removal(created_at: str, reason: str) -> dict[str, object]:
655
656
 
656
657
  def _merge_queue_answers(
657
658
  all_removals: list[dict[str, object]],
659
+ all_rules: list[object] = MAIN_BRANCH_RULES,
658
660
  ) -> object:
659
661
  def _answer(url: str, token: str, all_payload_fields: object) -> object:
660
662
  if url.endswith("/pulls/1442"):
661
663
  return QUEUED_PULL_REQUEST
662
664
  if url.endswith("/ccr/review_threads"):
663
665
  return []
666
+ if "/rules/branches/" in url:
667
+ return all_rules
664
668
  if url.endswith("/graphql"):
665
669
  return _merge_queue_document(all_removals)
666
670
  raise AssertionError(url)
@@ -668,6 +672,47 @@ def _merge_queue_answers(
668
672
  return _answer
669
673
 
670
674
 
675
+ def _refuse_graphql(answer: object) -> object:
676
+ def _answer(url: str, token: str, all_payload_fields: object) -> object:
677
+ if url.endswith("/graphql"):
678
+ raise agent_merge_check.MergeCheckError("HTTP Error 403: Forbidden")
679
+ return answer(url, token, all_payload_fields)
680
+
681
+ return _answer
682
+
683
+
684
+ def test_a_base_without_a_merge_queue_may_merge_when_graphql_is_refused(
685
+ monkeypatch: pytest.MonkeyPatch,
686
+ capsys: pytest.CaptureFixture[str],
687
+ ) -> None:
688
+ monkeypatch.setenv("GH_TOKEN", "token")
689
+ monkeypatch.setattr(
690
+ agent_merge_check,
691
+ "_request_json",
692
+ _refuse_graphql(_merge_queue_answers([], all_rules=[])),
693
+ )
694
+ exit_code = agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"])
695
+ assert exit_code == 0
696
+ assert capsys.readouterr().out.startswith(MERGE_VERDICT_LABEL)
697
+
698
+
699
+ def test_a_merge_queue_base_names_the_refused_graphql_read(
700
+ monkeypatch: pytest.MonkeyPatch,
701
+ capsys: pytest.CaptureFixture[str],
702
+ ) -> None:
703
+ monkeypatch.setenv("GH_TOKEN", "token")
704
+ monkeypatch.setattr(
705
+ agent_merge_check, "_request_json", _refuse_graphql(_merge_queue_answers([]))
706
+ )
707
+ exit_code = agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"])
708
+ captured = capsys.readouterr()
709
+ assert exit_code == 2
710
+ assert captured.out == ""
711
+ assert "merge queue" in captured.err
712
+ assert "GraphQL" in captured.err
713
+ assert "HTTP Error 403: Forbidden" in captured.err
714
+
715
+
671
716
  def _run_queued_main(
672
717
  monkeypatch: pytest.MonkeyPatch,
673
718
  capsys: pytest.CaptureFixture[str],
@@ -772,13 +817,11 @@ def test_read_merge_queue_ejection_compares_failed_checks_removals_to_the_head(
772
817
  expected_ejection: bool,
773
818
  ) -> None:
774
819
  monkeypatch.setattr(
775
- agent_merge_check,
776
- "_request_json",
777
- lambda url, token, all_payload_fields: _merge_queue_document(all_removals),
820
+ agent_merge_check, "_request_json", _merge_queue_answers(all_removals)
778
821
  )
779
822
  assert (
780
823
  agent_merge_check.read_merge_queue_ejection(
781
- "jl-cmd/claude-dev-env", 1442, "token"
824
+ "jl-cmd/claude-dev-env", 1442, "main", "token"
782
825
  )
783
826
  is expected_ejection
784
827
  )
@@ -915,8 +958,8 @@ def _unstable_answers(
915
958
  return PULL_REQUEST_5330
916
959
  if parsed.path.endswith("/ccr/review_threads"):
917
960
  return all_threads or []
918
- if parsed.path.endswith("/graphql"):
919
- return _merge_queue_document([])
961
+ if "/rules/branches/" in parsed.path:
962
+ return []
920
963
  if parsed.path.endswith("/check-runs"):
921
964
  return {"check_runs": _requested_page(parsed.query, all_check_runs)}
922
965
  if parsed.path.endswith("/status"):