sase-github 0.1.1__tar.gz → 0.1.3__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 (37) hide show
  1. sase_github-0.1.3/.release-please-manifest.json +3 -0
  2. {sase_github-0.1.1 → sase_github-0.1.3}/CHANGELOG.md +25 -0
  3. {sase_github-0.1.1 → sase_github-0.1.3}/PKG-INFO +1 -1
  4. {sase_github-0.1.1 → sase_github-0.1.3}/README.md +1 -0
  5. sase_github-0.1.3/docs/architecture.md +108 -0
  6. {sase_github-0.1.1 → sase_github-0.1.3}/docs/configuration.md +7 -6
  7. {sase_github-0.1.1 → sase_github-0.1.3}/pyproject.toml +1 -1
  8. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/workspace_plugin.py +17 -15
  9. {sase_github-0.1.1 → sase_github-0.1.3}/tests/test_workspace_plugin.py +35 -9
  10. sase_github-0.1.1/.release-please-manifest.json +0 -3
  11. sase_github-0.1.1/docs/architecture.md +0 -105
  12. {sase_github-0.1.1 → sase_github-0.1.3}/.github/workflows/ci.yml +0 -0
  13. {sase_github-0.1.1 → sase_github-0.1.3}/.github/workflows/pr-title.yml +0 -0
  14. {sase_github-0.1.1 → sase_github-0.1.3}/.github/workflows/publish.yml +0 -0
  15. {sase_github-0.1.1 → sase_github-0.1.3}/.sase_beads/beads.db +0 -0
  16. {sase_github-0.1.1 → sase_github-0.1.3}/.sase_beads/config.json +0 -0
  17. {sase_github-0.1.1 → sase_github-0.1.3}/.sase_beads/issues.jsonl +0 -0
  18. {sase_github-0.1.1 → sase_github-0.1.3}/CLAUDE.md +0 -0
  19. {sase_github-0.1.1 → sase_github-0.1.3}/Justfile +0 -0
  20. {sase_github-0.1.1 → sase_github-0.1.3}/LICENSE +0 -0
  21. {sase_github-0.1.1 → sase_github-0.1.3}/docs/xprompts.md +0 -0
  22. {sase_github-0.1.1 → sase_github-0.1.3}/release-please-config.json +0 -0
  23. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/__init__.py +0 -0
  24. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/config.py +0 -0
  25. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/default_config.yml +0 -0
  26. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/plugin.py +0 -0
  27. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/scripts/__init__.py +0 -0
  28. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/scripts/gh_setup.py +0 -0
  29. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/scripts/new_pr_desc_get_context.py +0 -0
  30. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/gh.yml +0 -0
  31. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/new_pr_desc.yml +0 -0
  32. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/pr_diff.yml +0 -0
  33. {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/prdd.yml +0 -0
  34. {sase_github-0.1.1 → sase_github-0.1.3}/tests/__init__.py +0 -0
  35. {sase_github-0.1.1 → sase_github-0.1.3}/tests/test_github_plugin.py +0 -0
  36. {sase_github-0.1.1 → sase_github-0.1.3}/tests/test_submit_with_recorded_pr.py +0 -0
  37. {sase_github-0.1.1 → sase_github-0.1.3}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.3"
3
+ }
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.3](https://github.com/sase-org/sase-github/compare/v0.1.2...v0.1.3) (2026-06-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * write project display names for repo refs ([65ddc1d](https://github.com/sase-org/sase-github/commit/65ddc1d6dd9efe152897449ffa7eb421d8694802))
9
+
10
+
11
+ ### Documentation
12
+
13
+ * add tiny field note ([d1d6563](https://github.com/sase-org/sase-github/commit/d1d6563822f3e48ec2504fed9156d1d0bbabb8a1))
14
+
15
+ ## [0.1.2](https://github.com/sase-org/sase-github/compare/v0.1.1...v0.1.2) (2026-06-13)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * restore sase-github PyPI publish workflow ([077e91a](https://github.com/sase-org/sase-github/commit/077e91a57e836856d1e263cdc348cf3761516b4c))
21
+ * use trusted publisher workflow path ([6a9fea0](https://github.com/sase-org/sase-github/commit/6a9fea00701428903f086ac871ac6c103521ae1d))
22
+
23
+
24
+ ### Documentation
25
+
26
+ * add PyPI version badge ([58f0145](https://github.com/sase-org/sase-github/commit/58f014514664d6c8402b04bbaea37e0402099312))
27
+
3
28
  ## 0.1.1 (2026-06-09)
4
29
 
5
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sase-github
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: GitHub VCS plugin for sase
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,5 +1,6 @@
1
1
  # sase-github — GitHub VCS Plugin for sase
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/sase-github?logo=pypi&logoColor=white)](https://pypi.org/project/sase-github/)
3
4
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
4
5
  [![mypy](https://img.shields.io/badge/type_checker-mypy-blue.svg)](https://mypy-lang.org/)
5
6
  [![pytest](https://img.shields.io/badge/tests-pytest-blue.svg)](https://docs.pytest.org/)
@@ -0,0 +1,108 @@
1
+ # Architecture
2
+
3
+ sase-github is structured as two pluggy-based plugins that integrate with sase core via Python entry points.
4
+
5
+ ## Plugin System
6
+
7
+ ### Entry Points
8
+
9
+ Registered in `pyproject.toml`:
10
+
11
+ | Entry Point | Plugin | Purpose |
12
+ | --------------------------- | ----------------------- | --------------------------------------------------------------- |
13
+ | `sase_vcs:github` | `GitHubPlugin` | VCS operations (push, PR creation, PR info) |
14
+ | `sase_workspace:github` | `GitHubWorkspacePlugin` | Workspace orchestration (ref resolution, submission, mail prep) |
15
+ | `sase_xprompts:sase_github` | — | Makes `#gh`, `#new_pr_desc`, `#prdd` xprompts discoverable |
16
+ | `sase_config:sase_github` | — | Contributes `default_config.yml` to the sase config chain |
17
+
18
+ ### GitHubPlugin (`plugin.py`)
19
+
20
+ Extends `GitCommon` from sase core. Handles low-level VCS operations by wrapping `git` and `gh` CLI commands.
21
+
22
+ **Hook implementations:**
23
+
24
+ | Hook | Behavior |
25
+ | --------------------------- | ------------------------------------------------------------------------------------- |
26
+ | `vcs_classify_repo()` | Claims repos with `github.com` in their origin URL |
27
+ | `vcs_get_change_url()` | Returns PR URL via `gh pr view --json url` |
28
+ | `vcs_get_cl_number()` | Returns PR number via `gh pr view --json number` |
29
+ | `vcs_mail()` | Pushes branch (`git push -u origin`) and creates PR if needed (`gh pr create --fill`) |
30
+ | `vcs_create_pull_request()` | Creates a PR with an AI-generated title and body |
31
+
32
+ ### GitHubWorkspacePlugin (`workspace_plugin.py`)
33
+
34
+ Handles higher-level workflow orchestration. Implements workspace hooks for GitHub-hosted projects.
35
+
36
+ **Hook implementations:**
37
+
38
+ | Hook | Behavior |
39
+ | -------------------------------------- | --------------------------------------------------------------------------------- |
40
+ | `ws_get_workflow_metadata()` | Returns metadata for the `gh` workflow type (ref pattern `#gh`, vcs family `git`) |
41
+ | `ws_detect_workflow_type()` | Returns `"gh"` for repos with a remote origin URL (non-local) |
42
+ | `ws_get_change_label()` | Returns `"PR"` for GitHub projects |
43
+ | `ws_resolve_ref()` | Resolves `#gh` references (see [Reference Resolution](#reference-resolution)) |
44
+ | `ws_extract_change_identifier()` | Extracts PR number from GitHub PR URLs |
45
+ | `ws_generate_submitted_check_script()` | Generates a bash script that checks if a PR is merged via `gh pr view` |
46
+ | `ws_supports_reviewer_comments()` | Returns `False` for GitHub URLs (critique comments not supported) |
47
+ | `ws_get_workspace_directory()` | Ensures git clone exists via `ensure_git_clone()` |
48
+ | `ws_prepare_mail()` | Displays branch/description and prompts user before pushing |
49
+ | `ws_format_commit_description()` | Prepends `[project]` prefix to commit messages |
50
+ | `ws_submit()` | Submits a ChangeSpec by merging its PR via `gh pr merge --merge --delete-branch` |
51
+
52
+ ## Reference Resolution
53
+
54
+ The `resolve_gh_ref()` function supports three dispatch modes for `#gh` references:
55
+
56
+ ### Mode 1: Repo Path (`user/project`)
57
+
58
+ When the ref contains `/`, it's treated as a GitHub repo path:
59
+
60
+ - Derives workspace from `~/projects/github/<user>/<project>/`
61
+ - Clones the repo if it doesn't exist (SSH for orgs in `github_orgs`, HTTPS otherwise)
62
+ - Reuses an existing SASE ProjectSpec whose normalized `WORKSPACE_DIR` already points at that workspace
63
+ - Otherwise creates a canonical project name from the repo identity, normally `gh_<user>__<project>`
64
+ - Adds a deterministic suffix such as `-2` only if that canonical project name is already occupied by a different
65
+ project, `PROJECT_NAME`, or alias
66
+ - Sets `WORKSPACE_DIR` in the canonical project file
67
+ - Sets a useful `PROJECT_NAME` for the repo basename, suffixing duplicate basenames as `<project>_1`, `<project>_2`, and
68
+ higher
69
+ - Checks out the default branch
70
+
71
+ This preserves legacy basename projects. If `~/.sase/projects/foo/foo.sase` already points at
72
+ `~/projects/github/foo-org/foo/`, resolving `#gh:foo-org/foo` keeps using project `foo`; it does not rename or migrate
73
+ the ProjectSpec. Existing auto-aliased GitHub projects are also left as-is and continue resolving through their alias.
74
+
75
+ ### Mode 2: Project Shorthand (`myproject`)
76
+
77
+ When the ref matches an existing project directory, `PROJECT_NAME`, or project alias:
78
+
79
+ - Resolves friendly names before project-directory lookup, so a generated `PROJECT_NAME` like `foo_1` points at its
80
+ canonical directory-key project
81
+ - Looks up `~/.sase/projects/<name>/<name>.sase` (legacy `.gp` is read as a fallback)
82
+ - Reads `WORKSPACE_DIR` from the project file
83
+ - Checks out the default branch
84
+
85
+ ### Mode 3: ChangeSpec Name
86
+
87
+ When the ref matches an existing ChangeSpec:
88
+
89
+ - Searches all changespecs for a matching name
90
+ - Reads `WORKSPACE_DIR` from the changespec's project file
91
+ - Checks out `origin/<name>` (the ChangeSpec's branch)
92
+
93
+ ## Submission Flow
94
+
95
+ When submitting a GitHub ChangeSpec (`ws_submit`):
96
+
97
+ 1. Kill and persist all running processes on the ChangeSpec
98
+ 2. Verify no active child ChangeSpecs exist
99
+ 3. Claim a workspace and checkout the ChangeSpec branch
100
+ 4. Check for an existing PR on the branch
101
+ 5. Merge via `gh pr merge --merge --delete-branch`
102
+ 6. Finalize submission (update ChangeSpec status)
103
+ 7. Release the workspace
104
+
105
+ ## Config Helper
106
+
107
+ `config.py` provides `get_github_orgs()` which reads the `github_orgs` list from the merged sase config. This determines
108
+ whether repos are cloned via SSH (for orgs the user has push access to) or HTTPS.
@@ -35,8 +35,8 @@ Currently the default config defines:
35
35
  ## Workspace Layout
36
36
 
37
37
  Primary GitHub workspaces are stored under `~/projects/github/<user>/<project>/` when first resolved from a
38
- `#gh(user/project)` reference. Numbered parallel-work checkouts follow SASE's shared `workspace.root` policy: by
39
- default they live under the platform state-root namespace, while explicit `workspace.root: adjacent` keeps the legacy
38
+ `#gh(user/project)` reference. Numbered parallel-work checkouts follow SASE's shared `workspace.root` policy: by default
39
+ they live under the platform state-root namespace, while explicit `workspace.root: adjacent` keeps the legacy
40
40
  `~/projects/github/<user>/<project>_<N>/` sibling layout.
41
41
 
42
42
  ## Project Files
@@ -47,9 +47,10 @@ when you first use an `#gh:<user>/<project>` ref.
47
47
 
48
48
  For new `owner/repo` refs, the project name is based on the full GitHub identity, normally `gh_<user>__<project>`, so
49
49
  two owners can have repositories with the same basename. If that canonical name is already occupied by a different
50
- project or alias, sase-github adds a deterministic suffix such as `-2`.
50
+ project, `PROJECT_NAME`, or alias, sase-github adds a deterministic suffix such as `-2`.
51
51
 
52
- sase-github also writes a short `PROJECT_ALIASES` value for the repo basename when it is valid and useful. The first
53
- `owner/foo` repo can get alias `foo`; a second `owner/foo` repo gets the next available alias such as `foo-2`. Existing
52
+ sase-github also writes `PROJECT_NAME` to the repo basename when it is valid and useful. The first `owner/foo` repo can
53
+ get `PROJECT_NAME: foo`; a second `owner/foo` repo gets the next available display name such as `foo_1`. Existing
54
54
  basename ProjectSpecs are reused when their `WORKSPACE_DIR` already matches the GitHub workspace, so no automatic
55
- migration or rename is required. Inspect or adjust generated aliases with `sase project alias`.
55
+ migration or rename is required. Existing auto-aliased GitHub projects are also left unchanged and keep resolving via
56
+ their `PROJECT_ALIASES` entry.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sase-github"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "GitHub VCS plugin for sase"
9
9
  requires-python = ">=3.12"
10
10
  license = "MIT"
@@ -401,7 +401,7 @@ def _find_project_record_for_alias(
401
401
  alias: str,
402
402
  ) -> ProjectRecordWire | None:
403
403
  for record in records:
404
- if alias in record.aliases:
404
+ if alias == getattr(record, "display_name", None) or alias in record.aliases:
405
405
  return record
406
406
  return None
407
407
 
@@ -422,10 +422,12 @@ def _canonical_project_name_base(user: str, project: str) -> str:
422
422
  return base
423
423
 
424
424
 
425
- def _project_name_or_aliases(records: Sequence[ProjectRecordWire]) -> set[str]:
425
+ def _project_refs(records: Sequence[ProjectRecordWire]) -> set[str]:
426
426
  occupied: set[str] = set()
427
427
  for record in records:
428
428
  occupied.add(record.project_name)
429
+ if display_name := getattr(record, "display_name", None):
430
+ occupied.add(display_name)
429
431
  occupied.update(record.aliases)
430
432
  return occupied
431
433
 
@@ -436,7 +438,7 @@ def _allocate_canonical_project_name(
436
438
  records: Sequence[ProjectRecordWire],
437
439
  ) -> str:
438
440
  base = _canonical_project_name_base(user, project)
439
- occupied = _project_name_or_aliases(records)
441
+ occupied = _project_refs(records)
440
442
 
441
443
  candidate = base
442
444
  suffix = 2
@@ -450,7 +452,7 @@ def _project_file_for(projects_base: Path, project_name: str) -> str:
450
452
  return preferred_project_spec_path(str(projects_base / project_name), project_name)
451
453
 
452
454
 
453
- def _ensure_useful_repo_alias(
455
+ def _ensure_useful_repo_name(
454
456
  project_name: str,
455
457
  repo_name: str,
456
458
  *,
@@ -460,24 +462,24 @@ def _ensure_useful_repo_alias(
460
462
  return
461
463
 
462
464
  from sase.project_aliases import (
463
- allocate_project_alias,
464
- ensure_project_alias_locked,
465
+ allocate_project_name,
466
+ ensure_project_name_locked,
465
467
  )
466
468
 
467
469
  attempts = 3
468
470
  for attempt in range(attempts):
469
471
  records = _list_project_records(projects_base)
470
- alias = allocate_project_alias(
472
+ display_name = allocate_project_name(
471
473
  repo_name,
472
474
  records,
473
475
  project_name=project_name,
474
476
  )
475
- if alias == project_name:
477
+ if display_name == project_name:
476
478
  return
477
479
  try:
478
- ensure_project_alias_locked(
480
+ ensure_project_name_locked(
479
481
  project_name,
480
- alias,
482
+ display_name,
481
483
  projects_root=projects_base,
482
484
  )
483
485
  return
@@ -520,15 +522,15 @@ def _resolve_repo_path_ref(user: str, project: str) -> ResolvedRef:
520
522
  project_file = _project_file_for(projects_base, project_name)
521
523
  if not set_workspace_dir(project_file, primary_workspace_dir):
522
524
  raise ValueError(f"Failed to write WORKSPACE_DIR for '{project_name}'")
525
+ _ensure_useful_repo_name(
526
+ project_name,
527
+ project,
528
+ projects_base=projects_base,
529
+ )
523
530
  else:
524
531
  project_name = existing_record.project_name
525
532
  project_file = existing_record.project_file
526
533
 
527
- _ensure_useful_repo_alias(
528
- project_name,
529
- project,
530
- projects_base=projects_base,
531
- )
532
534
  checkout_target = get_default_branch(primary_workspace_dir)
533
535
 
534
536
  return ResolvedRef(
@@ -38,7 +38,7 @@ class TestResolveGhRef:
38
38
  @patch(
39
39
  "sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
40
40
  )
41
- def test_repo_path_creates_canonical_project_and_alias(
41
+ def test_repo_path_creates_canonical_project_and_name(
42
42
  self, mock_branch: MagicMock
43
43
  ) -> None:
44
44
  with tempfile.TemporaryDirectory() as d:
@@ -61,12 +61,13 @@ class TestResolveGhRef:
61
61
  assert result.primary_workspace_dir == primary
62
62
  assert result.checkout_target == "origin/main"
63
63
  assert f"WORKSPACE_DIR: {primary}\n" in content
64
- assert "PROJECT_ALIASES: myrepo\n" in content
64
+ assert "PROJECT_NAME: myrepo\n" in content
65
+ assert "PROJECT_ALIASES" not in content
65
66
 
66
67
  @patch(
67
68
  "sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
68
69
  )
69
- def test_duplicate_repo_basename_gets_distinct_alias(
70
+ def test_duplicate_repo_basename_gets_distinct_name(
70
71
  self, mock_branch: MagicMock
71
72
  ) -> None:
72
73
  with tempfile.TemporaryDirectory() as d:
@@ -82,8 +83,8 @@ class TestResolveGhRef:
82
83
  second_file = Path(second.project_file)
83
84
  assert first.project_name == "gh_foo-org__foo"
84
85
  assert second.project_name == "gh_bar-org__foo"
85
- assert "PROJECT_ALIASES: foo\n" in first_file.read_text(encoding="utf-8")
86
- assert "PROJECT_ALIASES: foo-2\n" in second_file.read_text(encoding="utf-8")
86
+ assert "PROJECT_NAME: foo\n" in first_file.read_text(encoding="utf-8")
87
+ assert "PROJECT_NAME: foo_1\n" in second_file.read_text(encoding="utf-8")
87
88
 
88
89
  @patch(
89
90
  "sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
@@ -108,6 +109,30 @@ class TestResolveGhRef:
108
109
  assert result.project_file == str(project_file)
109
110
  assert "PROJECT_ALIASES" not in content
110
111
 
112
+ @patch(
113
+ "sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
114
+ )
115
+ def test_repo_path_reuses_existing_auto_aliased_project_without_migration(
116
+ self, mock_branch: MagicMock
117
+ ) -> None:
118
+ with tempfile.TemporaryDirectory() as d:
119
+ home = Path(d)
120
+ primary = _github_workspace(home, "alice", "myrepo")
121
+ project_file = _write_project(
122
+ home,
123
+ "gh_alice__myrepo",
124
+ f"WORKSPACE_DIR: {primary}\nPROJECT_ALIASES: myrepo\nNAME: legacy\n",
125
+ )
126
+ path_patch, env_patch = _home_patches(home)
127
+ with path_patch, env_patch:
128
+ result = resolve_gh_ref("alice/myrepo")
129
+
130
+ content = project_file.read_text(encoding="utf-8")
131
+ assert result.project_name == "gh_alice__myrepo"
132
+ assert result.project_file == str(project_file)
133
+ assert "PROJECT_ALIASES: myrepo\n" in content
134
+ assert "PROJECT_NAME" not in content
135
+
111
136
  @patch(
112
137
  "sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
113
138
  )
@@ -129,7 +154,8 @@ class TestResolveGhRef:
129
154
  content = Path(result.project_file).read_text(encoding="utf-8")
130
155
  assert result.project_name == "gh_alice__foo"
131
156
  assert result.primary_workspace_dir == primary
132
- assert "PROJECT_ALIASES: foo-2\n" in content
157
+ assert "PROJECT_NAME: foo_1\n" in content
158
+ assert "PROJECT_ALIASES" not in content
133
159
 
134
160
  @patch(
135
161
  "sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
@@ -150,14 +176,14 @@ class TestResolveGhRef:
150
176
  result = resolve_gh_ref("alice/foo")
151
177
 
152
178
  assert result.project_name == "gh_alice__foo-2"
153
- assert "PROJECT_ALIASES: foo\n" in Path(result.project_file).read_text(
179
+ assert "PROJECT_NAME: foo\n" in Path(result.project_file).read_text(
154
180
  encoding="utf-8"
155
181
  )
156
182
 
157
183
  @patch(
158
184
  "sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
159
185
  )
160
- def test_project_alias_shorthand_resolves_canonical_project(
186
+ def test_project_name_shorthand_resolves_canonical_project(
161
187
  self, mock_branch: MagicMock
162
188
  ) -> None:
163
189
  with tempfile.TemporaryDirectory() as d:
@@ -168,7 +194,7 @@ class TestResolveGhRef:
168
194
  with path_patch, env_patch:
169
195
  resolve_gh_ref("foo-org/foo")
170
196
  canonical = resolve_gh_ref("bar-org/foo")
171
- alias = resolve_gh_ref("foo-2")
197
+ alias = resolve_gh_ref("foo_1")
172
198
 
173
199
  assert alias.project_name == canonical.project_name
174
200
  assert alias.project_file == canonical.project_file
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.1.1"
3
- }
@@ -1,105 +0,0 @@
1
- # Architecture
2
-
3
- sase-github is structured as two pluggy-based plugins that integrate with sase core via Python entry points.
4
-
5
- ## Plugin System
6
-
7
- ### Entry Points
8
-
9
- Registered in `pyproject.toml`:
10
-
11
- | Entry Point | Plugin | Purpose |
12
- |---|---|---|
13
- | `sase_vcs:github` | `GitHubPlugin` | VCS operations (push, PR creation, PR info) |
14
- | `sase_workspace:github` | `GitHubWorkspacePlugin` | Workspace orchestration (ref resolution, submission, mail prep) |
15
- | `sase_xprompts:sase_github` | — | Makes `#gh`, `#new_pr_desc`, `#prdd` xprompts discoverable |
16
- | `sase_config:sase_github` | — | Contributes `default_config.yml` to the sase config chain |
17
-
18
- ### GitHubPlugin (`plugin.py`)
19
-
20
- Extends `GitCommon` from sase core. Handles low-level VCS operations by wrapping `git` and `gh` CLI commands.
21
-
22
- **Hook implementations:**
23
-
24
- | Hook | Behavior |
25
- |---|---|
26
- | `vcs_classify_repo()` | Claims repos with `github.com` in their origin URL |
27
- | `vcs_get_change_url()` | Returns PR URL via `gh pr view --json url` |
28
- | `vcs_get_cl_number()` | Returns PR number via `gh pr view --json number` |
29
- | `vcs_mail()` | Pushes branch (`git push -u origin`) and creates PR if needed (`gh pr create --fill`) |
30
- | `vcs_create_pull_request()` | Creates a PR with an AI-generated title and body |
31
-
32
- ### GitHubWorkspacePlugin (`workspace_plugin.py`)
33
-
34
- Handles higher-level workflow orchestration. Implements workspace hooks for GitHub-hosted projects.
35
-
36
- **Hook implementations:**
37
-
38
- | Hook | Behavior |
39
- |---|---|
40
- | `ws_get_workflow_metadata()` | Returns metadata for the `gh` workflow type (ref pattern `#gh`, vcs family `git`) |
41
- | `ws_detect_workflow_type()` | Returns `"gh"` for repos with a remote origin URL (non-local) |
42
- | `ws_get_change_label()` | Returns `"PR"` for GitHub projects |
43
- | `ws_resolve_ref()` | Resolves `#gh` references (see [Reference Resolution](#reference-resolution)) |
44
- | `ws_extract_change_identifier()` | Extracts PR number from GitHub PR URLs |
45
- | `ws_generate_submitted_check_script()` | Generates a bash script that checks if a PR is merged via `gh pr view` |
46
- | `ws_supports_reviewer_comments()` | Returns `False` for GitHub URLs (critique comments not supported) |
47
- | `ws_get_workspace_directory()` | Ensures git clone exists via `ensure_git_clone()` |
48
- | `ws_prepare_mail()` | Displays branch/description and prompts user before pushing |
49
- | `ws_format_commit_description()` | Prepends `[project]` prefix to commit messages |
50
- | `ws_submit()` | Submits a ChangeSpec by merging its PR via `gh pr merge --merge --delete-branch` |
51
-
52
- ## Reference Resolution
53
-
54
- The `resolve_gh_ref()` function supports three dispatch modes for `#gh` references:
55
-
56
- ### Mode 1: Repo Path (`user/project`)
57
-
58
- When the ref contains `/`, it's treated as a GitHub repo path:
59
-
60
- - Derives workspace from `~/projects/github/<user>/<project>/`
61
- - Clones the repo if it doesn't exist (SSH for orgs in `github_orgs`, HTTPS otherwise)
62
- - Reuses an existing SASE ProjectSpec whose normalized `WORKSPACE_DIR` already points at that workspace
63
- - Otherwise creates a canonical project name from the repo identity, normally `gh_<user>__<project>`
64
- - Adds a deterministic suffix such as `-2` only if that canonical project name is already occupied by a different
65
- project or alias
66
- - Sets `WORKSPACE_DIR` in the canonical project file
67
- - Ensures a useful short alias for the repo basename, suffixing duplicate basenames as `<project>-2`, `<project>-3`,
68
- and higher
69
- - Checks out the default branch
70
-
71
- This preserves legacy basename projects. If `~/.sase/projects/foo/foo.sase` already points at
72
- `~/projects/github/foo-org/foo/`, resolving `#gh:foo-org/foo` keeps using project `foo`; it does not rename or migrate
73
- the ProjectSpec.
74
-
75
- ### Mode 2: Project Shorthand (`myproject`)
76
-
77
- When the ref matches an existing project directory or project alias:
78
-
79
- - Resolves aliases before project-directory lookup, so a generated alias like `foo-2` points at its canonical project
80
- - Looks up `~/.sase/projects/<name>/<name>.sase` (legacy `.gp` is read as a fallback)
81
- - Reads `WORKSPACE_DIR` from the project file
82
- - Checks out the default branch
83
-
84
- ### Mode 3: ChangeSpec Name
85
-
86
- When the ref matches an existing ChangeSpec:
87
- - Searches all changespecs for a matching name
88
- - Reads `WORKSPACE_DIR` from the changespec's project file
89
- - Checks out `origin/<name>` (the ChangeSpec's branch)
90
-
91
- ## Submission Flow
92
-
93
- When submitting a GitHub ChangeSpec (`ws_submit`):
94
-
95
- 1. Kill and persist all running processes on the ChangeSpec
96
- 2. Verify no active child ChangeSpecs exist
97
- 3. Claim a workspace and checkout the ChangeSpec branch
98
- 4. Check for an existing PR on the branch
99
- 5. Merge via `gh pr merge --merge --delete-branch`
100
- 6. Finalize submission (update ChangeSpec status)
101
- 7. Release the workspace
102
-
103
- ## Config Helper
104
-
105
- `config.py` provides `get_github_orgs()` which reads the `github_orgs` list from the merged sase config. This determines whether repos are cloned via SSH (for orgs the user has push access to) or HTTPS.
File without changes
File without changes
File without changes
File without changes