sase-github 0.2.9__tar.gz → 0.2.11__tar.gz

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.
Files changed (46) hide show
  1. sase_github-0.2.11/.release-please-manifest.json +3 -0
  2. {sase_github-0.2.9 → sase_github-0.2.11}/CHANGELOG.md +14 -0
  3. {sase_github-0.2.9 → sase_github-0.2.11}/PKG-INFO +2 -2
  4. {sase_github-0.2.9 → sase_github-0.2.11}/pyproject.toml +2 -2
  5. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/scripts/gh_setup.py +72 -1
  6. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/scripts/new_pr_desc_get_context.py +1 -4
  7. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/workspace_plugin.py +2 -13
  8. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/xprompts/gh.yml +0 -1
  9. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_gh_workspace_claims.py +10 -0
  10. sase_github-0.2.11/tests/test_new_pr_desc_get_context.py +68 -0
  11. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_workspace_plugin.py +107 -0
  12. sase_github-0.2.9/.release-please-manifest.json +0 -3
  13. {sase_github-0.2.9 → sase_github-0.2.11}/.github/workflows/ci.yml +0 -0
  14. {sase_github-0.2.9 → sase_github-0.2.11}/.github/workflows/pr-title.yml +0 -0
  15. {sase_github-0.2.9 → sase_github-0.2.11}/.github/workflows/publish.yml +0 -0
  16. {sase_github-0.2.9 → sase_github-0.2.11}/.sase_beads/beads.db +0 -0
  17. {sase_github-0.2.9 → sase_github-0.2.11}/.sase_beads/config.json +0 -0
  18. {sase_github-0.2.9 → sase_github-0.2.11}/.sase_beads/issues.jsonl +0 -0
  19. {sase_github-0.2.9 → sase_github-0.2.11}/CLAUDE.md +0 -0
  20. {sase_github-0.2.9 → sase_github-0.2.11}/Justfile +0 -0
  21. {sase_github-0.2.9 → sase_github-0.2.11}/LICENSE +0 -0
  22. {sase_github-0.2.9 → sase_github-0.2.11}/README.md +0 -0
  23. {sase_github-0.2.9 → sase_github-0.2.11}/docs/architecture.md +0 -0
  24. {sase_github-0.2.9 → sase_github-0.2.11}/docs/configuration.md +0 -0
  25. {sase_github-0.2.9 → sase_github-0.2.11}/docs/xprompts.md +0 -0
  26. {sase_github-0.2.9 → sase_github-0.2.11}/release-please-config.json +0 -0
  27. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/__init__.py +0 -0
  28. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/config.py +0 -0
  29. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/default_config.yml +0 -0
  30. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/errors.py +0 -0
  31. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/plugin.py +0 -0
  32. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/scripts/__init__.py +0 -0
  33. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/scripts/gh_release.py +0 -0
  34. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/task_types.py +0 -0
  35. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/xprompts/new_pr_desc.yml +0 -0
  36. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/xprompts/pr_diff.yml +0 -0
  37. {sase_github-0.2.9 → sase_github-0.2.11}/src/sase_github/xprompts/prdd.yml +0 -0
  38. {sase_github-0.2.9 → sase_github-0.2.11}/tests/__init__.py +0 -0
  39. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_ci_install_contract.py +0 -0
  40. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_config.py +0 -0
  41. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_github_issues.py +0 -0
  42. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_github_plugin.py +0 -0
  43. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_github_pull_requests.py +0 -0
  44. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_submit_with_recorded_pr.py +0 -0
  45. {sase_github-0.2.9 → sase_github-0.2.11}/tests/test_task_types.py +0 -0
  46. {sase_github-0.2.9 → sase_github-0.2.11}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.11"
3
+ }
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.11](https://github.com/sase-org/sase-github/compare/v0.2.10...v0.2.11) (2026-09-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **github:** preflight managed clone origins in setup ([d09ee25](https://github.com/sase-org/sase-github/commit/d09ee25f3fac18cd874afb78143d8dfcb6449709))
9
+
10
+ ## [0.2.10](https://github.com/sase-org/sase-github/compare/v0.2.9...v0.2.10) (2026-09-08)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **gh:** reject non-github setup workspaces ([f032e91](https://github.com/sase-org/sase-github/commit/f032e91b0b10a5a71af913b7d2ad424e1700b93c))
16
+
3
17
  ## [0.2.9](https://github.com/sase-org/sase-github/compare/v0.2.8...v0.2.9) (2026-08-29)
4
18
 
5
19
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sase-github
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: GitHub VCS plugin for sase
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.12
8
- Requires-Dist: sase>=0.11.0
8
+ Requires-Dist: sase>=0.17.0
9
9
  Provides-Extra: dev
10
10
  Requires-Dist: mypy; extra == 'dev'
11
11
  Requires-Dist: pytest; extra == 'dev'
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sase-github"
7
- version = "0.2.9"
7
+ version = "0.2.11"
8
8
  description = "GitHub VCS plugin for sase"
9
9
  requires-python = ">=3.12"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
12
- dependencies = ["sase>=0.11.0"]
12
+ dependencies = ["sase>=0.17.0"]
13
13
 
14
14
  [project.optional-dependencies]
15
15
  dev = [
@@ -1,6 +1,7 @@
1
1
  """Setup step for the #gh xprompt workflow."""
2
2
 
3
3
  import os
4
+ import subprocess
4
5
  import sys
5
6
 
6
7
  from sase.core.occupancy_guard import OccupancyCaller, ensure_workspace_not_occupied
@@ -16,7 +17,10 @@ from sase.running_field import (
16
17
  from sase.sdd.store import materialize_sdd_store
17
18
  from sase.workspace_provider import resolve_ref
18
19
  from sase.workspace_provider.occupant import new_occupant_record, write_occupant_record
19
- from sase.workspace_provider.utils import ensure_workspace_checkout
20
+ from sase.workspace_provider.utils import (
21
+ ensure_workspace_checkout,
22
+ reconcile_managed_checkout_origin,
23
+ )
20
24
 
21
25
  _CALLER_TAG = "gh-setup"
22
26
 
@@ -84,6 +88,32 @@ def main(
84
88
  pinned=not release,
85
89
  )
86
90
 
91
+ try:
92
+ if (
93
+ pre_allocated
94
+ and workspace_num > 1
95
+ and os.path.isdir(workspace_dir.rstrip("/"))
96
+ ):
97
+ reconcile_managed_checkout_origin(
98
+ workspace_dir,
99
+ primary_workspace_dir=resolved.primary_workspace_dir,
100
+ assume_managed_checkout=True,
101
+ )
102
+ _assert_github_vcs_provider(
103
+ workspace_dir=workspace_dir,
104
+ primary_workspace_dir=resolved.primary_workspace_dir,
105
+ )
106
+ except Exception:
107
+ if not pre_allocated:
108
+ release_workspace(
109
+ project_file,
110
+ workspace_num,
111
+ workflow_name,
112
+ cl_name,
113
+ caller_tag=_CALLER_TAG,
114
+ )
115
+ raise
116
+
87
117
  # Refuse to hand this checkout to `prepare`/`checkout` if another live
88
118
  # agent still occupies it. Runs on the pre_allocated branch too: the
89
119
  # launcher already wrote its own occupant record, and the conflict
@@ -254,6 +284,47 @@ def _materialize_or_release(
254
284
  return workspace_dir
255
285
 
256
286
 
287
+ def _assert_github_vcs_provider(
288
+ *,
289
+ workspace_dir: str,
290
+ primary_workspace_dir: str,
291
+ ) -> None:
292
+ provider_name = _detect_vcs_provider_name(workspace_dir)
293
+ if provider_name == "github":
294
+ return
295
+ actual_origin = _origin_url(workspace_dir)
296
+ expected_origin = _origin_url(primary_workspace_dir)
297
+ raise RuntimeError(
298
+ "GitHub workflow refused workspace because its VCS provider resolved "
299
+ f"to {provider_name or 'unknown'!r}, not 'github'. "
300
+ f"workspace origin: {actual_origin or '<unreadable>'}; "
301
+ f"expected GitHub origin: {expected_origin or '<unreadable>'}"
302
+ )
303
+
304
+
305
+ def _detect_vcs_provider_name(workspace_dir: str) -> str | None:
306
+ from sase.vcs_provider import detect_vcs
307
+
308
+ return detect_vcs(workspace_dir)
309
+
310
+
311
+ def _origin_url(workspace_dir: str) -> str | None:
312
+ try:
313
+ result = subprocess.run(
314
+ ["git", "config", "--get", "remote.origin.url"],
315
+ cwd=workspace_dir,
316
+ capture_output=True,
317
+ text=True,
318
+ check=False,
319
+ timeout=10,
320
+ )
321
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
322
+ return None
323
+ if result.returncode != 0:
324
+ return None
325
+ return result.stdout.strip() or None
326
+
327
+
257
328
  def _describe_workspace_occupant(project_file: str, workspace_num: int) -> str | None:
258
329
  occupants = [
259
330
  claim
@@ -7,10 +7,7 @@ from pathlib import Path
7
7
 
8
8
  from sase.workspace_provider.utils import get_default_branch, parse_workspace_dir
9
9
 
10
- try:
11
- from sase.ace.patch import find_all_patches
12
- except ImportError: # Older supported SASE releases expose only ChangeSpec names.
13
- from sase.ace.changespec import find_all_changespecs as find_all_patches
10
+ from sase.ace.patch import find_all_patches
14
11
 
15
12
  try:
16
13
  from sase.core.paths import ( # type: ignore[attr-defined]
@@ -17,19 +17,8 @@ from collections.abc import Mapping, Sequence
17
17
  from pathlib import Path
18
18
  from typing import TYPE_CHECKING, Any, Literal
19
19
 
20
- try:
21
- from sase.ace.patch import Patch, find_all_patches
22
- from sase.ace.patch.project_spec_path import preferred_project_spec_path
23
- except ImportError: # Older supported SASE releases expose only ChangeSpec names.
24
- from sase.ace.changespec import ( # type: ignore[no-redef]
25
- # Legacy import names are adapted to canonical Patch locals.
26
- ChangeSpec as Patch,
27
- # Legacy import names are adapted to canonical Patch locals.
28
- find_all_changespecs as find_all_patches,
29
- )
30
- from sase.ace.changespec.project_spec_path import ( # type: ignore[no-redef]
31
- preferred_project_spec_path,
32
- )
20
+ from sase.ace.patch import Patch, find_all_patches
21
+ from sase.ace.patch.project_spec_path import preferred_project_spec_path
33
22
  from sase.workspace_provider import (
34
23
  ExternalRepoCloneResult,
35
24
  ResolvedRef,
@@ -1,5 +1,4 @@
1
1
  description: Check out a GitHub ref in an isolated workspace and show resulting changes.
2
- wraps_all: true
3
2
  tags: vcs, rollover
4
3
 
5
4
  input:
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from collections.abc import Iterator
6
7
  from pathlib import Path
7
8
  from types import SimpleNamespace
8
9
  from unittest.mock import MagicMock, patch
@@ -29,6 +30,15 @@ from sase_github.workspace_plugin import GitHubWorkspacePlugin
29
30
  ROOT = Path(__file__).resolve().parents[1]
30
31
 
31
32
 
33
+ @pytest.fixture(autouse=True)
34
+ def _github_provider_guard_passes() -> Iterator[None]:
35
+ with (
36
+ patch("sase_github.scripts.gh_setup._assert_github_vcs_provider"),
37
+ patch("sase_github.scripts.gh_setup.reconcile_managed_checkout_origin"),
38
+ ):
39
+ yield
40
+
41
+
32
42
  def _write_project_file(
33
43
  tmp_path: Path, running_claims: list[WorkspaceClaim] | None = None
34
44
  ) -> str:
@@ -0,0 +1,68 @@
1
+ """Tests for the #new_pr_desc xprompt's get-context script."""
2
+
3
+ from pathlib import Path
4
+ from types import SimpleNamespace
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+
9
+ from sase_github.scripts import new_pr_desc_get_context
10
+
11
+
12
+ def test_main_reports_context_for_found_patch(
13
+ tmp_path: Path,
14
+ capsys: pytest.CaptureFixture[str],
15
+ ) -> None:
16
+ """The script resolves the Patch through the canonical ``sase.ace.patch`` API."""
17
+ workspace_dir = tmp_path / "workspace"
18
+ workspace_dir.mkdir()
19
+ diff_dir = tmp_path / "gh-diffs"
20
+ diff_dir.mkdir()
21
+ entry = SimpleNamespace(
22
+ name="feature",
23
+ file_path=str(workspace_dir / "feature.sase"),
24
+ description="Add feature",
25
+ )
26
+
27
+ with (
28
+ patch(
29
+ "sase_github.scripts.new_pr_desc_get_context.find_all_patches",
30
+ return_value=[entry],
31
+ ),
32
+ patch(
33
+ "sase_github.scripts.new_pr_desc_get_context.parse_workspace_dir",
34
+ return_value=str(workspace_dir),
35
+ ),
36
+ patch(
37
+ "sase_github.scripts.new_pr_desc_get_context.get_default_branch",
38
+ return_value="origin/main",
39
+ ),
40
+ patch(
41
+ "sase_github.scripts.new_pr_desc_get_context.subprocess.run",
42
+ return_value=SimpleNamespace(returncode=0, stdout=""),
43
+ ),
44
+ patch(
45
+ "sase_github.scripts.new_pr_desc_get_context.get_sase_managed_tmpdir",
46
+ return_value=str(diff_dir),
47
+ ),
48
+ ):
49
+ new_pr_desc_get_context.main(name="feature")
50
+
51
+ out = capsys.readouterr().out
52
+ assert "error=\n" in out
53
+ assert "description=Add feature\n" in out
54
+ assert "default_branch=main\n" in out
55
+ assert "branch_name=feature\n" in out
56
+
57
+
58
+ def test_main_reports_error_for_missing_patch(
59
+ capsys: pytest.CaptureFixture[str],
60
+ ) -> None:
61
+ with patch(
62
+ "sase_github.scripts.new_pr_desc_get_context.find_all_patches",
63
+ return_value=[],
64
+ ):
65
+ new_pr_desc_get_context.main(name="missing")
66
+
67
+ out = capsys.readouterr().out
68
+ assert "error=Patch 'missing' not found\n" in out
@@ -2062,6 +2062,58 @@ class TestWsResolveRef:
2062
2062
 
2063
2063
 
2064
2064
  class TestGhSetup:
2065
+ def test_preallocated_workspace_reconciles_origin_before_provider_assertion(
2066
+ self, tmp_path: Path
2067
+ ) -> None:
2068
+ from sase_github.scripts import gh_setup
2069
+
2070
+ workspace_dir = str(tmp_path / "prealloc")
2071
+ Path(workspace_dir).mkdir()
2072
+ resolved = ResolvedRef(
2073
+ project_file="/tmp/gh_acme__widget.sase",
2074
+ project_name="gh_acme__widget",
2075
+ primary_workspace_dir="/work/widget/",
2076
+ checkout_target="origin/main",
2077
+ canonical_ref="gh_acme__widget",
2078
+ )
2079
+ order: list[str] = []
2080
+
2081
+ def reconcile(*_args: object, **_kwargs: object) -> None:
2082
+ order.append("reconcile")
2083
+
2084
+ def assert_provider(*_args: object, **_kwargs: object) -> None:
2085
+ order.append("provider")
2086
+
2087
+ with (
2088
+ patch("sase_github.scripts.gh_setup.resolve_ref", return_value=resolved),
2089
+ patch.dict(
2090
+ os.environ,
2091
+ {
2092
+ "SASE_GH_PRE_ALLOCATED": "1",
2093
+ "SASE_GH_WORKSPACE_NUM": "13",
2094
+ "SASE_GH_WORKSPACE_DIR": workspace_dir,
2095
+ },
2096
+ ),
2097
+ patch("sase_github.scripts.gh_setup.materialize_sdd_store"),
2098
+ patch(
2099
+ "sase_github.scripts.gh_setup.reconcile_managed_checkout_origin",
2100
+ side_effect=reconcile,
2101
+ ) as mock_reconcile,
2102
+ patch(
2103
+ "sase_github.scripts.gh_setup._assert_github_vcs_provider",
2104
+ side_effect=assert_provider,
2105
+ ),
2106
+ patch("sase_github.scripts.gh_setup.ensure_workspace_not_occupied"),
2107
+ ):
2108
+ gh_setup.main(gh_ref="acme/widget", n=None, release=True)
2109
+
2110
+ assert order[:2] == ["reconcile", "provider"]
2111
+ mock_reconcile.assert_called_once_with(
2112
+ workspace_dir,
2113
+ primary_workspace_dir="/work/widget/",
2114
+ assume_managed_checkout=True,
2115
+ )
2116
+
2065
2117
  def test_materializes_sdd_store_after_atomic_claim(self) -> None:
2066
2118
  from sase_github.scripts import gh_setup
2067
2119
 
@@ -2096,6 +2148,7 @@ class TestGhSetup:
2096
2148
  "sase_github.scripts.gh_setup.materialize_sdd_store",
2097
2149
  side_effect=materialize,
2098
2150
  ) as materialize_mock,
2151
+ patch("sase_github.scripts.gh_setup._assert_github_vcs_provider"),
2099
2152
  ):
2100
2153
  gh_setup.main(gh_ref="acme/widget", n=None, release=False)
2101
2154
 
@@ -2142,6 +2195,60 @@ class TestGhSetup:
2142
2195
  caller_tag="gh-setup",
2143
2196
  )
2144
2197
 
2198
+ def test_non_github_workspace_provider_releases_claimed_slot(self) -> None:
2199
+ from sase_github.scripts import gh_setup
2200
+
2201
+ resolved = ResolvedRef(
2202
+ project_file="/tmp/gh_acme__widget.sase",
2203
+ project_name="gh_acme__widget",
2204
+ primary_workspace_dir="/work/widget/",
2205
+ checkout_target="origin/main",
2206
+ canonical_ref="gh_acme__widget",
2207
+ )
2208
+
2209
+ with (
2210
+ patch("sase_github.scripts.gh_setup.resolve_ref", return_value=resolved),
2211
+ patch.dict(os.environ, {"SASE_GH_PRE_ALLOCATED": "0"}),
2212
+ patch(
2213
+ "sase_github.scripts.gh_setup.ensure_workspace_checkout",
2214
+ return_value="/work/widget_7/",
2215
+ ),
2216
+ patch(
2217
+ "sase_github.scripts.gh_setup.claim_next_axe_workspace",
2218
+ return_value=7,
2219
+ ),
2220
+ patch(
2221
+ "sase_github.scripts.gh_setup.materialize_sdd_store",
2222
+ return_value=None,
2223
+ ),
2224
+ patch(
2225
+ "sase_github.scripts.gh_setup._detect_vcs_provider_name",
2226
+ return_value="bare_git",
2227
+ ),
2228
+ patch(
2229
+ "sase_github.scripts.gh_setup._origin_url",
2230
+ side_effect=lambda path: {
2231
+ "/work/widget_7/": "/work/widget/",
2232
+ "/work/widget/": "git@github.com:acme/widget.git",
2233
+ }[path],
2234
+ ),
2235
+ patch("sase_github.scripts.gh_setup.release_workspace") as release,
2236
+ pytest.raises(RuntimeError) as exc_info,
2237
+ ):
2238
+ gh_setup.main(gh_ref="acme/widget", n=None, release=False)
2239
+
2240
+ message = str(exc_info.value)
2241
+ assert "VCS provider resolved to 'bare_git'" in message
2242
+ assert "workspace origin: /work/widget/" in message
2243
+ assert "expected GitHub origin: git@github.com:acme/widget.git" in message
2244
+ release.assert_called_once_with(
2245
+ "/tmp/gh_acme__widget.sase",
2246
+ 7,
2247
+ "gh-acme/widget",
2248
+ None,
2249
+ caller_tag="gh-setup",
2250
+ )
2251
+
2145
2252
 
2146
2253
  # ── detect_workflow_type (via plugin) ────────────────────────────────
2147
2254
 
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.2.9"
3
- }
File without changes
File without changes
File without changes
File without changes
File without changes