sase-github 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,5 @@
1
+ """sase-github: GitHub VCS plugin for sase."""
2
+
3
+ from sase_github.plugin import GitHubPlugin
4
+
5
+ __all__ = ["GitHubPlugin"]
sase_github/plugin.py ADDED
@@ -0,0 +1,44 @@
1
+ """GitHub VCS plugin implementation.
2
+
3
+ Handles git repositories hosted on GitHub (or similar hosted services).
4
+ Inherits shared git operations from :class:`GitCommon` and adds
5
+ GitHub-specific methods (``mail`` with PR creation, ``get_cl_number``
6
+ and ``get_change_url`` via ``gh`` CLI).
7
+ """
8
+
9
+ from sase.vcs_provider._hookspec import hookimpl
10
+ from sase.vcs_provider.plugins._git_common import GitCommon
11
+
12
+
13
+ class GitHubPlugin(GitCommon):
14
+ """Pluggy plugin for GitHub-hosted git repositories."""
15
+
16
+ @hookimpl
17
+ def vcs_get_change_url(self, cwd: str) -> tuple[bool, str | None]:
18
+ out = self._run(["gh", "pr", "view", "--json", "url", "-q", ".url"], cwd)
19
+ if out.success:
20
+ url = out.stdout.strip()
21
+ return (True, url) if url else (True, None)
22
+ return (True, None)
23
+
24
+ @hookimpl
25
+ def vcs_get_cl_number(self, cwd: str) -> tuple[bool, str | None]:
26
+ out = self._run(["gh", "pr", "view", "--json", "number", "-q", ".number"], cwd)
27
+ if out.success:
28
+ number = out.stdout.strip()
29
+ return (True, number) if number else (True, None)
30
+ return (True, None)
31
+
32
+ @hookimpl
33
+ def vcs_mail(self, revision: str, cwd: str) -> tuple[bool, str | None]:
34
+ out = self._run(["git", "push", "-u", "origin", revision], cwd)
35
+ if not out.success:
36
+ return self._to_result(out, "git push")
37
+ pr_check = self._run(
38
+ ["gh", "pr", "view", "--json", "number", "-q", ".number"], cwd
39
+ )
40
+ if not pr_check.success:
41
+ pr_create = self._run(["gh", "pr", "create", "--fill"], cwd)
42
+ if not pr_create.success:
43
+ return self._to_result(pr_create, "gh pr create")
44
+ return (True, None)
@@ -0,0 +1,87 @@
1
+ wraps_all: true
2
+
3
+ input:
4
+ - name: gh_ref
5
+ type: word
6
+ - name: n
7
+ type: int
8
+ default: null
9
+ - name: release
10
+ type: bool
11
+ default: true
12
+
13
+ steps:
14
+ - name: setup
15
+ python: |
16
+ from sase.scripts.gh_setup import main
17
+ main(
18
+ gh_ref={{ gh_ref | tojson }},
19
+ n={{ n | tojson }},
20
+ release={{ release | tojson }},
21
+ )
22
+ output:
23
+ project_name: word
24
+ project_file: path
25
+ workspace_dir: path
26
+ workspace_num: int
27
+ checkout_target: word
28
+ primary_workspace_dir: path
29
+ should_release: bool
30
+
31
+ - name: prepare
32
+ bash: |
33
+ # Save diff backup if workspace is dirty
34
+ if ! git diff --quiet HEAD 2>/dev/null; then
35
+ git diff HEAD > "/tmp/gh-workspace-backup-$(date +%s).patch"
36
+ fi
37
+ git checkout . && git clean -fd
38
+ git fetch --quiet
39
+ git pull --rebase --quiet 2>&1 || true
40
+ echo "head_before=$(git rev-parse HEAD)"
41
+ echo "success=true"
42
+ output: { success: bool, head_before: word }
43
+
44
+ - name: inject
45
+ prompt_part: ""
46
+
47
+ - name: release
48
+ if: "{{ setup.should_release }}"
49
+ python: |
50
+ from sase.running_field import release_workspace
51
+
52
+ release_workspace(
53
+ {{ setup.project_file | tojson }},
54
+ {{ setup.workspace_num }},
55
+ f"gh-{{ gh_ref }}",
56
+ {{ gh_ref | tojson }} if "/" not in {{ gh_ref | tojson }} else None,
57
+ )
58
+ print("released=true")
59
+ output: { released: bool }
60
+
61
+ - name: diff
62
+ bash: |
63
+ head_before="{{ prepare.head_before }}"
64
+ if [ -z "$head_before" ]; then
65
+ exit 0
66
+ fi
67
+ head_now=$(git rev-parse HEAD)
68
+ diff_file=$(mktemp /tmp/sase-gh-XXXXXX.diff)
69
+ if [ "$head_now" != "$head_before" ]; then
70
+ # Commits were made — show only the last commit's diff
71
+ git diff HEAD~1 HEAD > "$diff_file" 2>/dev/null
72
+ commit_msg=$(git log -1 --format='%s' HEAD)
73
+ echo "meta_commit_message=$commit_msg"
74
+ else
75
+ # No commits — show uncommitted changes
76
+ git diff HEAD > "$diff_file" 2>/dev/null
77
+ git ls-files --others --exclude-standard -z 2>/dev/null \
78
+ | while IFS= read -r -d '' f; do
79
+ git diff --no-index /dev/null "$f" 2>/dev/null
80
+ done >> "$diff_file"
81
+ fi
82
+ if [ -s "$diff_file" ]; then
83
+ echo "diff_path=$diff_file"
84
+ else
85
+ rm -f "$diff_file"
86
+ fi
87
+ output: { diff_path: path, meta_commit_message: line }
@@ -0,0 +1,64 @@
1
+ input:
2
+ - name: name
3
+ type: word
4
+
5
+ steps:
6
+ - name: get_context
7
+ python: |
8
+ from sase.scripts.new_pr_desc_get_context import main
9
+ main(name={{ name | tojson }})
10
+ output:
11
+ error: line
12
+ description: line
13
+ diff_file: path
14
+ commits: text
15
+ workspace_dir: path
16
+ default_branch: word
17
+ branch_name: word
18
+
19
+ - name: generate
20
+ if: "{{ not get_context.error }}"
21
+ agent: |
22
+ Generate a PR title and body for the following changes.
23
+
24
+ ## ChangeSpec Description
25
+ {{ get_context.description }}
26
+
27
+ ## Commits
28
+ {{ get_context.commits }}
29
+
30
+ ## Diff
31
+ @{{ get_context.diff_file }}
32
+
33
+ ## Instructions
34
+ - PR title: Use conventional commit format (e.g., "feat: ...", "fix: ..."), under 72 characters
35
+ - PR body: Start with "## Summary" followed by 1-3 bullet points
36
+
37
+ Output EXACTLY in this format (no extra text):
38
+ TITLE: <your title here>
39
+ BODY:
40
+ <your body here>
41
+
42
+ - name: apply
43
+ if: "{{ not get_context.error }}"
44
+ bash: |
45
+ # Check if PR exists for this branch
46
+ PR_NUMBER=$(gh pr view "{{ get_context.branch_name }}" --json number --jq '.number' 2>/dev/null || echo "")
47
+ if [ -z "$PR_NUMBER" ]; then
48
+ echo "applied=false"
49
+ echo "No existing PR found for branch {{ get_context.branch_name }}"
50
+ exit 0
51
+ fi
52
+
53
+ # Extract title and body from response
54
+ RESPONSE="{{ _response }}"
55
+ TITLE=$(echo "$RESPONSE" | grep -m1 '^TITLE: ' | sed 's/^TITLE: //')
56
+ BODY=$(echo "$RESPONSE" | sed -n '/^BODY:$/,$ p' | tail -n +2)
57
+
58
+ if [ -n "$TITLE" ] && [ -n "$BODY" ]; then
59
+ gh pr edit "$PR_NUMBER" --title "$TITLE" --body "$BODY" 2>/dev/null && echo "applied=true" || echo "applied=false"
60
+ else
61
+ echo "applied=false"
62
+ echo "Could not parse title/body from response"
63
+ fi
64
+ output: { applied: bool }
@@ -0,0 +1,88 @@
1
+ input:
2
+ - name: name
3
+ type: word
4
+
5
+ steps:
6
+ - name: create_branch
7
+ bash: |
8
+ BRANCH="{{ name }}"
9
+ git checkout -b "$BRANCH" 2>&1
10
+ git push -u origin "$BRANCH" 2>&1
11
+ echo "branch_name=$BRANCH"
12
+ echo "meta_changespec=$BRANCH"
13
+ output:
14
+ branch_name: word
15
+
16
+ - name: inject
17
+ prompt_part: ""
18
+
19
+ - name: create_changespec
20
+ hidden: true
21
+ python: |
22
+ from sase.scripts.pr_create_changespec import main
23
+ main(
24
+ name={{ name | tojson }},
25
+ prompt={{ _prompt | tojson }},
26
+ response={{ _response | tojson }},
27
+ )
28
+ output:
29
+ success: bool
30
+ cl_name: word
31
+ project_file: path
32
+ default_branch: word
33
+
34
+ - name: create_pr
35
+ hidden: true
36
+ if: "{{ create_changespec.success | default(false) }}"
37
+ bash: |
38
+ BRANCH="{{ create_branch.branch_name }}"
39
+ DEFAULT_BRANCH="{{ create_changespec.default_branch }}"
40
+
41
+ # Check for existing PR first
42
+ EXISTING_URL=$(gh pr view "$BRANCH" --json url --jq '.url' 2>/dev/null || echo "")
43
+ if [ -n "$EXISTING_URL" ]; then
44
+ echo "pr_url=$EXISTING_URL"
45
+ echo "success=true"
46
+ exit 0
47
+ fi
48
+
49
+ # Get PR title from first commit
50
+ PR_TITLE=$(git log --format='%s' "origin/$DEFAULT_BRANCH".."$BRANCH" 2>/dev/null | tail -1)
51
+ if [ -z "$PR_TITLE" ]; then
52
+ PR_TITLE="Agent changes on branch $BRANCH"
53
+ fi
54
+
55
+ PR_URL=$(gh pr create \
56
+ --base "$DEFAULT_BRANCH" \
57
+ --head "$BRANCH" \
58
+ --title "$PR_TITLE" \
59
+ --body "Automated PR from sase." 2>&1) || true
60
+
61
+ if echo "$PR_URL" | grep -q "^http"; then
62
+ echo "pr_url=$PR_URL"
63
+ echo "success=true"
64
+ else
65
+ echo "pr_url="
66
+ echo "success=false"
67
+ echo "Error: $PR_URL" >&2
68
+ fi
69
+ output: { pr_url: line, success: bool }
70
+
71
+ - name: update_cl
72
+ hidden: true
73
+ if: "{{ create_pr.success | default(false) }}"
74
+ python: |
75
+ from sase.status_state_machine import update_changespec_cl_atomic
76
+
77
+ project_file = {{ create_changespec.project_file | tojson }}
78
+ cl_name = {{ create_changespec.cl_name | tojson }}
79
+ pr_url = {{ create_pr.pr_url | tojson }}
80
+
81
+ if pr_url and pr_url.startswith("http"):
82
+ update_changespec_cl_atomic(project_file, cl_name, pr_url)
83
+ print("updated=true")
84
+ print(f"pr_url={pr_url}")
85
+ else:
86
+ print("updated=false")
87
+ print(f"pr_url={pr_url}")
88
+ output: { updated: bool, pr_url: line }
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: sase-github
3
+ Version: 0.1.0
4
+ Summary: GitHub VCS plugin for sase
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: sase>=0.1.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: mypy; extra == 'dev'
11
+ Requires-Dist: pytest; extra == 'dev'
12
+ Requires-Dist: pytest-cov; extra == 'dev'
13
+ Requires-Dist: pytest-mock; extra == 'dev'
14
+ Requires-Dist: ruff; extra == 'dev'
@@ -0,0 +1,10 @@
1
+ sase_github/__init__.py,sha256=xeuI37R-jN0XKnfVC6KOw0mLOkocjYTYGz-vdC-xshU,120
2
+ sase_github/plugin.py,sha256=rQg042xufx8MzMyAqYnc9kshbYI1nQDI9LoInQbaIwI,1727
3
+ sase_github/xprompts/gh.yml,sha256=xtuTDvox_Py_p9cA6z_VUbgp72IZG2AU4kVV8IXKaXA,2443
4
+ sase_github/xprompts/new_pr_desc.yml,sha256=40wASzusygfOdeIsys_Dg4mL4hpjuaA36zUotuit1DI,1889
5
+ sase_github/xprompts/pr.yml,sha256=ZLWfsqTVwo_g3DVyNQqLHdxE1PHcyyB0M7irCnNS7r0,2485
6
+ sase_github-0.1.0.dist-info/METADATA,sha256=8Ide9MGsYI271C5kunOaHwF9ew1Adyj1o_36t4qACzY,403
7
+ sase_github-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ sase_github-0.1.0.dist-info/entry_points.txt,sha256=78aQwHeD5IhKgyirp9LXuSqwSwpINmK8ksAToIjaQ3Y,95
9
+ sase_github-0.1.0.dist-info/licenses/LICENSE,sha256=KPJBpPDM-x7UoMJM17Ze0qRlTgtDJcRFUcXu6Xrt9zk,1068
10
+ sase_github-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [sase_vcs]
2
+ github = sase_github.plugin:GitHubPlugin
3
+
4
+ [sase_xprompts]
5
+ sase_github = sase_github
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bryan Bugyi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.