Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ gh2 pat create --account <login> --name <name> --owner <login> \
--expires-in <days|none> [--yes --token-output <new-file|->]
gh2 support login
gh2 support create --subject <subject> --body <body> [--account <identifier>] [--yes]
gh2 support view <ticket> [--scope personal/0] [--output json]
gh2 support reply <ticket> --body <body> [--scope personal/0] [--yes]
```

### Administration boundary
Expand Down Expand Up @@ -206,11 +208,15 @@ organization-owned token into pending approval after creation.

### GitHub Support tickets

`gh2 support create` signs in to `support.github.com` through GitHub OAuth using the session captured by `gh2 support login`. It does not open a browser. The default is a live-authenticated dry run; add `--yes` only after reviewing the exact ticket.
The Support commands sign in to `support.github.com` through GitHub OAuth using the session captured by `gh2 support login`. They do not open a browser. Ticket creation and replies default to a live-authenticated dry run; add `--yes` only after reviewing the exact write.

```bash
gh2 support login

# Read the original body and every reply in chronological order
gh2 support view 1234567
gh2 support view 1234567 --scope personal/0 --output json

gh2 support create \
--account "Circles Inc." \
--subject "Remove sensitive data from repository history" \
Expand All @@ -222,9 +228,15 @@ gh2 support create \
--subject "Remove sensitive data from repository history" \
--body-file ./ticket.md \
--yes

# Preview a reply; omit --yes unless posting was explicitly requested
gh2 support reply 1234567 --body-file ./reply.md
gh2 support reply 1234567 --body-file ./reply.md --yes
```

The command uses GitHub Support's authenticated web endpoint because GitHub does not publish a Support ticket REST or GraphQL API. Portal changes can therefore require a `gh2` update. If GitHub requires a captcha, the command refuses to submit and directs the operator to the portal.
`support view --output json` returns `ticket`, `subject`, `status`, `account`, `scope`, `created_at`, `author`, `body`, and chronological `comments` containing `id`, `author`, `created_at`, and `body`. Authentication cookies and form tokens are never part of the output.

The commands use GitHub Support's authenticated web pages because GitHub does not publish a Support ticket REST or GraphQL API. Portal changes can therefore require a `gh2` update. If GitHub requires a captcha, ticket creation refuses to submit and directs the operator to the portal.

## Webhook lifecycle

Expand Down
19 changes: 16 additions & 3 deletions skills/guide/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
name: gh2
description: Guide for GitHub Apps, installation approvals, deleted-repository restoration, organization PAT policies, fine-grained personal access tokens, and Support via the gh2 CLI
user-invocable: false
---

# gh2 CLI
Expand Down Expand Up @@ -188,11 +187,15 @@ copy the token into logs, table output, issue bodies, or pull request text.

## GitHub Support

Create a ticket without opening the Support portal in a browser:
Read or create a ticket without opening the Support portal in a browser:

```bash
gh2 support login

# Read the original body and every reply in chronological order
gh2 support view 1234567
gh2 support view 1234567 --scope personal/0 --output json

# Dry run: authenticates and prints the exact ticket without creating it
gh2 support create \
--account "Circles Inc." \
Expand All @@ -205,9 +208,19 @@ gh2 support create \
--subject "Remove sensitive data from repository history" \
--body-file ./ticket.md \
--yes

# Preview a reply; omit --yes unless posting was explicitly requested
gh2 support reply 1234567 --body-file ./reply.md
gh2 support reply 1234567 --body-file ./reply.md --yes
```

Use `--body -` to read the body from stdin. The command refuses submission if GitHub requires a captcha. Treat this as a web integration that may need updating when the Support portal changes.
Keep support tickets and replies minimal and action-oriented: state the exact action requested and include only the identifiers and evidence GitHub needs to perform it. Do not volunteer internal migration history, business context, or exhaustive verification logs unless GitHub asks.

Before running `gh2 support create ... --yes` or `gh2 support reply ... --yes`, always run the dry run, show the exact subject and body to the user, and obtain explicit approval for that text. A general instruction to handle the ticket is not approval to submit unseen wording.

`support view` is read-only. Its JSON output contains ticket metadata, the original `body`, and chronological `comments` with `id`, `author`, `created_at`, and `body`; it must never include authentication cookies or form tokens. Use `--scope personal/0` (or an organization scope) to skip account discovery when the scope is already known.

Use `--body -` to read a create/reply body from stdin. Write commands remain dry runs without `--yes`, and ticket creation refuses submission if GitHub requires a captcha. Treat Support commands as a web integration that may need updating when the portal changes.

## Default config path

Expand Down
2 changes: 2 additions & 0 deletions src/commands/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineCommand } from "citty";
import { loginCommand } from "../app/login.ts";
import { supportCreateCommand } from "./create.ts";
import { supportReplyCommand } from "./reply.ts";
import { supportViewCommand } from "./view.ts";

export const supportCommand = defineCommand({
meta: {
Expand All @@ -11,6 +12,7 @@ export const supportCommand = defineCommand({
subCommands: {
login: loginCommand,
create: supportCreateCommand,
view: supportViewCommand,
reply: supportReplyCommand,
},
});
103 changes: 103 additions & 0 deletions src/commands/support/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { defineCommand } from "citty";
import { loadAuth } from "../../lib/auth.ts";
import {
SupportSession,
type SupportTicketDetails,
} from "../../lib/support.ts";
import {
getOutputFormat,
printOutput,
type OutputFormat,
} from "../../lib/output.ts";

function ticketOutput(ticket: SupportTicketDetails, account: string | undefined) {
return {
ticket: ticket.ticketId,
subject: ticket.subject,
status: ticket.status,
account: account ?? ticket.author,
scope: ticket.scope,
Comment on lines +13 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The JSON and table account field is populated from the authenticated GitHub login, which is not necessarily the Support account represented by the ticket. Organization tickets will consequently report the viewer's login instead of the ticket account; derive this value from the parsed scope or expose the authenticated identity under a separate field. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ Organization ticket metadata reports the viewer account.
- ⚠️ JSON consumers receive an incorrect account field.
- ⚠️ Table output can confuse authors with Support accounts.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/commands/support/view.ts
**Line:** 13:19
**Comment:**
	*Api Mismatch: The JSON and table `account` field is populated from the authenticated GitHub login, which is not necessarily the Support account represented by the ticket. Organization tickets will consequently report the viewer's login instead of the ticket account; derive this value from the parsed scope or expose the authenticated identity under a separate field.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

created_at: ticket.createdAt,
author: ticket.author,
body: ticket.body,
comments: ticket.comments.map((comment) => ({
id: comment.id,
author: comment.author,
created_at: comment.createdAt,
body: comment.body,
})),
};
}

function printTicket(
ticket: SupportTicketDetails,
account: string | undefined,
format: OutputFormat,
): void {
const output = ticketOutput(ticket, account);
if (format === "json") {
printOutput(output, format);
return;
}

printOutput(
{
ticket: output.ticket,
subject: output.subject,
status: output.status,
account: output.account,
scope: output.scope,
created_at: output.created_at,
comments: output.comments.length,
},
format,
);
console.log(`\nBody — ${output.author}\n\n${output.body}`);
if (output.comments.length === 0) return;

console.log("\nComments");
for (const [index, comment] of output.comments.entries()) {
console.log(
`\n${index + 1}. ${comment.author} · ${comment.created_at} · ${comment.id}\n\n${comment.body}`,
);
}
}

export const supportViewCommand = defineCommand({
meta: {
name: "view",
description: "Show a GitHub Support ticket and its comments",
},
args: {
ticket: {
type: "positional",
description: "Ticket number, for example 1234567",
required: true,
},
scope: {
type: "string",
description:
"Ticket scope such as 'personal/0'; defaults to searching every accessible scope",
},
output: {
type: "string",
description: "Output format: json | table (default: table)",
default: "table",
},
},
async run({ args }) {
const ticket = String(args.ticket).replace(/^#/, "");
if (!/^\d+$/.test(ticket)) {
throw new Error(`"${args.ticket}" is not a ticket number.`);
}

const auth = await loadAuth();
const session = new SupportSession(auth);
await session.login();
const scopes = args.scope
? [args.scope.replace(/^\/?(tickets\/)?/, "")]
: ["personal/0", ...(await session.ticketScopes())];
const details = await session.viewTicket(ticket, [...new Set(scopes)]);
printTicket(details, auth.account, getOutputFormat(args.output));
},
});
Loading
Loading