-
-
Notifications
You must be signed in to change notification settings - Fork 4
✨ feat: add repo preflight onboarding command #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
SigureMo
merged 8 commits into
ShigureLab:main
from
ShigureNyako:feat/repo-preflight-onboarding
Mar 22, 2026
Merged
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
995c6b8
✨ feat: add repo preflight onboarding command
ShigureNyako d3c1d8e
🩹 fix: follow up repo preflight review feedback
ShigureNyako e8e2b43
🔀 merge upstream/main into feat/repo-preflight-onboarding
ShigureNyako a6edff9
🩹 fix: tighten repo preflight surface
ShigureNyako 0399c92
🎨 style: format repo preflight CLI tests
ShigureNyako 9fc32bb
🔀 merge upstream/main into feat/repo-preflight-onboarding
ShigureNyako 0bfa0e9
🩹 fix: detect parent repo for fork preflight
ShigureNyako 308b673
🔀 merge upstream/main into feat/repo-preflight-onboarding
ShigureNyako File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,238 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from gh_llm.github_api import GitHubClient | ||
| from gh_llm.invocation import display_command_with | ||
|
|
||
| if TYPE_CHECKING: | ||
| from gh_llm.models import RepoDocument, RepoPreflight | ||
|
|
||
|
|
||
| def register_repo_parser(subparsers: Any) -> None: | ||
| repo_parser = subparsers.add_parser("repo", help="Repository-level onboarding commands") | ||
| repo_subparsers = repo_parser.add_subparsers(dest="repo_command") | ||
|
|
||
| preflight_parser = repo_subparsers.add_parser( | ||
| "preflight", | ||
| help="show repo-level preflight summary", | ||
| ) | ||
| preflight_parser.add_argument("--repo", required=True, help="repository in OWNER/REPO format") | ||
| preflight_parser.set_defaults(handler=cmd_repo_preflight) | ||
|
|
||
|
|
||
| def cmd_repo_preflight(args: Any) -> int: | ||
| client = GitHubClient() | ||
| preflight = client.resolve_repo_preflight(str(args.repo)) | ||
| for line in render_repo_preflight(preflight): | ||
| print(line) | ||
| return 0 | ||
|
|
||
|
|
||
| def render_repo_preflight(preflight: RepoPreflight) -> list[str]: | ||
| return [ | ||
| *render_repo_frontmatter(preflight), | ||
| "", | ||
| *render_repo_summary(preflight), | ||
| *render_repo_onboarding_files(preflight), | ||
| *render_repo_branch_protection(preflight), | ||
| *render_repo_next_commands(preflight), | ||
| ] | ||
|
|
||
|
|
||
| def render_repo_frontmatter(preflight: RepoPreflight) -> list[str]: | ||
| lines = [ | ||
| "---", | ||
| f"repo: {preflight.owner}/{preflight.name}", | ||
| f"url: {preflight.url}", | ||
| f"default_branch: {preflight.default_branch}", | ||
| f"viewer_permission: {preflight.viewer_permission or 'UNKNOWN'}", | ||
| f"can_push: {str(preflight.can_push).lower()}", | ||
| f"fork_recommended: {str(preflight.fork_recommended).lower()}", | ||
| f"is_fork: {str(preflight.is_fork).lower()}", | ||
| f"tree_truncated: {str(preflight.tree_truncated).lower()}", | ||
| ] | ||
| if preflight.parent_repo: | ||
| lines.append(f"parent_repo: {preflight.parent_repo}") | ||
| if preflight.branch_protection is not None: | ||
| lines.append(f"branch_protection_pattern: {preflight.branch_protection.pattern}") | ||
| lines.append("---") | ||
| return lines | ||
|
|
||
|
|
||
| def render_repo_summary(preflight: RepoPreflight) -> list[str]: | ||
| permission = preflight.viewer_permission or "UNKNOWN" | ||
| https_clone_url = ( | ||
| preflight.url + ".git" if preflight.url else f"https://github.com/{preflight.owner}/{preflight.name}.git" | ||
| ) | ||
| lines = [ | ||
| "## Repository", | ||
| f"Description: {preflight.description or '(no description)'}", | ||
| f"Default branch: `{preflight.default_branch}`", | ||
| f"Homepage: {preflight.homepage_url or '(none)'}", | ||
| f"HTTPS clone: `{https_clone_url}`", | ||
| f"SSH clone: `{preflight.ssh_url or '(not available)'}`", | ||
| f"Push access: {'yes' if preflight.can_push else 'no'} (viewer_permission: {permission})", | ||
| f"Fork before push: {'yes' if preflight.fork_recommended else 'no'}", | ||
| ] | ||
| if preflight.is_fork and preflight.parent_repo: | ||
| lines.append(f"Parent repo: `{preflight.parent_repo}`") | ||
| if preflight.tree_truncated: | ||
| lines.append( | ||
| "Warning: recursive repository tree output was truncated; onboarding file detection used common-path fallback and may still be incomplete." | ||
| ) | ||
| lines.append("") | ||
| return lines | ||
|
|
||
|
|
||
| def render_repo_onboarding_files(preflight: RepoPreflight) -> list[str]: | ||
| repo = f"{preflight.owner}/{preflight.name}" | ||
| return [ | ||
| "## Onboarding Files", | ||
| *render_repo_document_group( | ||
| title="CONTRIBUTING", | ||
| docs=preflight.contributing_docs, | ||
| repo=repo, | ||
| default_branch=preflight.default_branch, | ||
| ), | ||
| *render_repo_document_group( | ||
| title="AGENTS", | ||
| docs=preflight.agents_docs, | ||
| repo=repo, | ||
| default_branch=preflight.default_branch, | ||
| ), | ||
| *render_repo_document_group( | ||
| title="PR Templates", | ||
| docs=preflight.pr_templates, | ||
| repo=repo, | ||
| default_branch=preflight.default_branch, | ||
| ), | ||
| *render_repo_document_group( | ||
| title="CODEOWNERS", | ||
| docs=preflight.codeowners_files, | ||
| repo=repo, | ||
| default_branch=preflight.default_branch, | ||
| ), | ||
| "", | ||
| ] | ||
|
|
||
|
|
||
| def render_repo_document_group( | ||
| *, | ||
| title: str, | ||
| docs: tuple[RepoDocument, ...], | ||
| repo: str, | ||
| default_branch: str, | ||
| ) -> list[str]: | ||
| lines = [f"### {title}"] | ||
| if not docs: | ||
| lines.append("(not found)") | ||
| return lines | ||
|
|
||
| for doc in docs: | ||
| browse_cmd = _gh_browse_command(repo=repo, default_branch=default_branch, path=doc.path) | ||
| lines.append(f"- `{doc.path}`") | ||
| lines.append(f" ⏎ open: `{browse_cmd}`") | ||
| return lines | ||
|
|
||
|
|
||
| def render_repo_branch_protection(preflight: RepoPreflight) -> list[str]: | ||
| lines = ["## Branch Protection"] | ||
| protection = preflight.branch_protection | ||
| if protection is None: | ||
| lines.append(f"Default branch `{preflight.default_branch}` is not protected.") | ||
| lines.append("") | ||
| return lines | ||
|
|
||
| if protection.source == "graphql": | ||
| lines.append(f"Matched rule: `{protection.pattern}`") | ||
| else: | ||
| lines.append(f"Protected branch: `{protection.pattern}`") | ||
| lines.append("Review-related rule details were not available from branch rule queries.") | ||
| if protection.requires_status_checks: | ||
| if protection.required_status_check_contexts: | ||
| lines.append( | ||
| "Required checks: " + ", ".join(f"`{name}`" for name in protection.required_status_check_contexts) | ||
| ) | ||
| else: | ||
| lines.append("Required checks: enabled, but GitHub did not return named contexts.") | ||
| else: | ||
| lines.append("Required checks: not enabled") | ||
|
|
||
| if protection.requires_approving_reviews is None: | ||
| lines.append("Approving reviews: unknown") | ||
| elif protection.requires_approving_reviews: | ||
| required = protection.required_approving_review_count or 1 | ||
| lines.append(f"Approving reviews: required ({required})") | ||
| else: | ||
| lines.append("Approving reviews: not required") | ||
|
|
||
| if protection.requires_code_owner_reviews is None: | ||
| lines.append("Code owner reviews: unknown") | ||
| else: | ||
| lines.append( | ||
| "Code owner reviews: " + ("required" if protection.requires_code_owner_reviews else "not required") | ||
| ) | ||
|
|
||
| if protection.is_admin_enforced is None: | ||
| lines.append("Admin enforcement: unknown") | ||
| else: | ||
| lines.append("Admin enforcement: " + ("enabled" if protection.is_admin_enforced else "disabled")) | ||
| lines.append("") | ||
| return lines | ||
|
|
||
|
|
||
| def render_repo_next_commands(preflight: RepoPreflight) -> list[str]: | ||
| repo = f"{preflight.owner}/{preflight.name}" | ||
| pr_target_repo = preflight.parent_repo if preflight.is_fork and preflight.parent_repo is not None else repo | ||
| lines = [ | ||
| "## Next Commands", | ||
| "These commands are inferred from the permission, onboarding-file, and default-branch protection signals above.", | ||
| ] | ||
|
|
||
| step = 1 | ||
| if preflight.fork_recommended: | ||
| lines.append(f"{step}. Create your fork before pushing:") | ||
| lines.append(f" ⏎ `gh repo fork {repo} --clone`") | ||
| else: | ||
| lines.append(f"{step}. Clone the repository:") | ||
| lines.append(f" ⏎ `gh repo clone {repo}`") | ||
| step += 1 | ||
|
|
||
| doc_steps = [ | ||
| ("Read contribution guidance", preflight.contributing_docs), | ||
| ("Read AGENTS instructions", preflight.agents_docs), | ||
| ("Inspect the PR template before opening a pull request", preflight.pr_templates), | ||
| ("Check CODEOWNERS to see who may review your changes", preflight.codeowners_files), | ||
| ] | ||
| for title, docs in doc_steps: | ||
| if not docs: | ||
| continue | ||
| lines.append(f"{step}. {title}:") | ||
| lines.append( | ||
| f" ⏎ `{_gh_browse_command(repo=repo, default_branch=preflight.default_branch, path=docs[0].path)}`" | ||
| ) | ||
| step += 1 | ||
|
|
||
| if pr_target_repo != repo: | ||
| lines.append(f"{step}. Open your PR against the parent repository:") | ||
| else: | ||
| lines.append(f"{step}. Open your PR against the default branch:") | ||
| lines.append(f" ⏎ `gh pr create --repo {pr_target_repo} --base {preflight.default_branch}`") | ||
| step += 1 | ||
|
|
||
| protection = preflight.branch_protection | ||
| if protection is not None and protection.requires_status_checks: | ||
| lines.append(f"{step}. Check required CI after the PR exists:") | ||
| lines.append(f" ⏎ `{display_command_with(f'pr checks --pr <pr_number> --repo {pr_target_repo}')}`") | ||
|
|
||
| lines.append("") | ||
| return lines | ||
|
|
||
|
|
||
| def _gh_browse_command(*, repo: str, default_branch: str, path: str) -> str: | ||
| return f"gh browse -R {repo} --branch {default_branch} {_shell_single_quote(path)}" | ||
|
|
||
|
|
||
| def _shell_single_quote(value: str) -> str: | ||
| return "'" + value.replace("'", "'\"'\"'") + "'" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.