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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,13 @@ gh2 support view 1234567
gh2 support view 1234567 --scope personal/0 --output json

gh2 support create \
--account "Circles Inc." \
--account "Example Corp." \
--subject "Remove sensitive data from repository history" \
--body-file ./ticket.md

# Submit the reviewed ticket
gh2 support create \
--account "Circles Inc." \
--account "Example Corp." \
--subject "Remove sensitive data from repository history" \
--body-file ./ticket.md \
--yes
Expand Down
6 changes: 3 additions & 3 deletions skills/guide/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ All config files are `github.<stage>.json` where `stage ∈ {local, prod}`:
```json
{
"appId": 2864083,
"name": "circlesac-yg2",
"name": "example-app",
"webhookSecret": "...",
"privateKey": "<base64-encoded PEM>"
}
Expand Down Expand Up @@ -198,13 +198,13 @@ 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." \
--account "Example Corp." \
--subject "Remove sensitive data from repository history" \
--body-file ./ticket.md

# Submit only after reviewing the dry run
gh2 support create \
--account "Circles Inc." \
--account "Example Corp." \
--subject "Remove sensitive data from repository history" \
--body-file ./ticket.md \
--yes
Expand Down
10 changes: 9 additions & 1 deletion src/lib/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,17 @@ export function parseSupportTicketDetails(html: string): SupportTicketDetails {
const ticketId = attribute(ticketTag, "data-ticket-id");
const orgType = attribute(ticketTag, "data-org-type");
const orgId = attribute(ticketTag, "data-org-id");
const status = html
const stateTag = html.match(
/<([a-z][\w-]*)\b[^>]*class="[^"]*\bState\b[^"]*"[^>]*>([\s\S]*?)<\/\1>/i,
);
const stateLabel = stateTag ? htmlToText(stateTag[2] ?? "").toLowerCase() : "";
const legacyState = stateTag?.[0]
.match(/class="[^"]*\bState--(open|closed)\b/i)?.[1]
?.toLowerCase();
const status =
stateLabel === "open" || stateLabel === "closed"
? stateLabel
: legacyState;
if (
!ticketId ||
!orgType ||
Expand Down
4 changes: 2 additions & 2 deletions tests/delete-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ describe("parseDeleteForm", () => {
describe("confirmValueFromPattern", () => {
it("reconstructs the app name from a case-insensitive char-class pattern", () => {
expect(
confirmValueFromPattern("[cC][iI][rR][cC][lL][eE][sS][aA][cC]-[yY][gG]2"),
).toBe("circlesac-yg2");
confirmValueFromPattern("[eE][xX][aA][mM][pP][lL][eE]-[aA][pP][pP]"),
).toBe("example-app");
});

it("keeps literal characters (digits, hyphens) as-is", () => {
Expand Down
48 changes: 24 additions & 24 deletions tests/pat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ const PAT_FORM_HTML = `
<form id="new_user_programmatic_access" action="/settings/personal-access-tokens" method="post">
<input type="hidden" name="authenticity_token" value="a&amp;b">
<input type="text" name="user_programmatic_access[name]" value="Priority reconciler">
<input type="hidden" name="target_name" value="melten-ai">
<input type="hidden" name="target_name" value="example-org">
<template>
<input type="hidden" name="user_programmatic_access[default_expires_at]" value="30">
</template>
<input type="hidden" name="user_programmatic_access[default_expires_at]" value="custom">
<input type="date" name="user_programmatic_access[custom_expires_at]" value="2026-09-01">
<textarea name="user_programmatic_access[description]">Policy automation</textarea>
<include-fragment src="/settings/personal-access-tokens/select-access?target_name=melten-ai&amp;issues=write"></include-fragment>
<include-fragment src="/settings/personal-access-tokens/select-access?target_name=example-org&amp;issues=write"></include-fragment>
</form>
</body>
</html>`;
Expand All @@ -47,7 +47,7 @@ const ACCESS_HTML = `
<input type="hidden" name="integration[default_permissions][issues]" value="write">
<input type="hidden" name="integration[default_permissions][metadata]" value="read">
<input type="hidden" name="integration[default_permissions][contents]" value="none">
<remote-input src="/settings/personal-access-tokens/suggestions?target_name=melten-ai&amp;experimental=1"></remote-input>
<remote-input src="/settings/personal-access-tokens/suggestions?target_name=example-org&amp;experimental=1"></remote-input>
<script type="application/json" data-target="react-partial.embeddedData">${JSON.stringify(
{
props: {
Expand All @@ -70,19 +70,19 @@ const OWNER_HTML = `
<button data-value="work-admin">work-admin</button>
</li>
<li data-actor-is-organization="true" data-fg-limit="366" data-fg-limit-label="366 days" data-fg-limit-exempt="false">
<button data-value="melten-ai">melten-ai</button>
<button data-value="example-org">example-org</button>
</li>`;

const REPOSITORY_HTML = `
<li>
<button data-value="1245612113">melten-ai/silicon-workbench</button>
<input type="hidden" name="repository_ids[]" value="1245612113">
<span class="owner css-truncate-target">melten-ai</span>/<span class="repo">silicon-workbench</span>
<button data-value="101">example-org/sample-repo</button>
<input type="hidden" name="repository_ids[]" value="101">
<span class="owner css-truncate-target">example-org</span>/<span class="repo">sample-repo</span>
</li>
<li>
<button data-value="1263434240">melten-ai/pcie_gen4_pipe_axis_tl</button>
<input type="hidden" name="repository_ids[]" value="1263434240">
<span class="owner">melten-ai</span>/<span class="repo css-truncate-target">pcie_gen4_pipe_axis_tl</span>
<button data-value="102">example-org/another-repo</button>
<input type="hidden" name="repository_ids[]" value="102">
<span class="owner">example-org</span>/<span class="repo css-truncate-target">another-repo</span>
</li>`;

describe("fine-grained PAT form parsing", () => {
Expand All @@ -92,7 +92,7 @@ describe("fine-grained PAT form parsing", () => {
expect(form!.account).toBe("work-admin");
expect(form!.action).toBe("/settings/personal-access-tokens");
expect(form!.accessPath).toBe(
"/settings/personal-access-tokens/select-access?target_name=melten-ai&issues=write",
"/settings/personal-access-tokens/select-access?target_name=example-org&issues=write",
);
expect(form!.fields).toContainEqual(["authenticity_token", "a&b"]);
expect(
Expand All @@ -119,7 +119,7 @@ describe("fine-grained PAT form parsing", () => {
maxExpirationLabel: undefined,
},
{
login: "melten-ai",
login: "example-org",
organization: true,
expirationExempt: false,
maxExpirationDays: 366,
Expand All @@ -130,11 +130,11 @@ describe("fine-grained PAT form parsing", () => {

it("reads live repository IDs without accepting other owners", () => {
expect(parseRepositoryOptions(REPOSITORY_HTML)).toEqual([
{ id: "1245612113", owner: "melten-ai", name: "silicon-workbench" },
{ id: "101", owner: "example-org", name: "sample-repo" },
{
id: "1263434240",
owner: "melten-ai",
name: "pcie_gen4_pipe_axis_tl",
id: "102",
owner: "example-org",
name: "another-repo",
},
]);
});
Expand Down Expand Up @@ -178,15 +178,15 @@ describe("fine-grained PAT request validation", () => {
it("normalizes selected repositories and validates their owner", () => {
expect(
parseRepositorySelection(
"silicon-workbench,melten-ai/pcie_gen4_pipe_axis_tl",
"melten-ai",
"sample-repo,example-org/another-repo",
"example-org",
),
).toEqual({
mode: "selected",
names: ["silicon-workbench", "pcie_gen4_pipe_axis_tl"],
names: ["sample-repo", "another-repo"],
});
expect(() =>
parseRepositorySelection("other/repo", "melten-ai"),
parseRepositorySelection("other/repo", "example-org"),
).toThrow(/does not belong/);
});

Expand All @@ -204,18 +204,18 @@ describe("fine-grained PAT request validation", () => {
name: "Priority reconciler",
description: "Policy automation",
reason: "Needed for issue reconciliation",
owner: "melten-ai",
owner: "example-org",
repositories: {
mode: "selected",
names: ["silicon-workbench", "pcie_gen4_pipe_axis_tl"],
names: ["sample-repo", "another-repo"],
},
repositoryOptions: parseRepositoryOptions(REPOSITORY_HTML),
permissions: { issues: "write", metadata: "read" },
});
expect(body.get("install_target")).toBe("selected");
expect(body.getAll("repository_ids[]")).toEqual([
"1245612113",
"1263434240",
"101",
"102",
]);
expect(body.get("integration[default_permissions][issues]")).toBe("write");
expect(body.get("integration[default_permissions][metadata]")).toBe("read");
Expand Down
16 changes: 13 additions & 3 deletions tests/support.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const personal: SupportAccount = {
const organization: SupportAccount = {
...personal,
id: "org-id",
identifier: "Circles Inc.",
identifier: "Example Corp.",
type: "Organization",
};

Expand Down Expand Up @@ -83,7 +83,7 @@ describe("Support ticket selection", () => {
});

it("matches an explicit account case-insensitively", () => {
expect(selectSupportAccount(bootstrap.accounts, "circles inc.")).toBe(
expect(selectSupportAccount(bootstrap.accounts, "example corp.")).toBe(
organization,
);
});
Expand Down Expand Up @@ -221,8 +221,18 @@ describe("support ticket view", () => {

it("reads a closed ticket without requiring a comment form", () => {
const closed = TICKET_DETAILS_HTML
.replace("State--open", "State--closed")
.replace(
'class="State State--open">Open',
'class="State bgColor-done-emphasis">Closed',
)
.replace(/<form id="js-ticket-comment-form"[\s\S]*?<\/form>/, "");
expect(parseSupportTicketDetails(closed).status).toBe("closed");
});

it("keeps compatibility with the legacy status class", () => {
const legacyClosed = TICKET_DETAILS_HTML
.replace("State--open", "State--closed")
.replace(">Open</span>", "></span>");
expect(parseSupportTicketDetails(legacyClosed).status).toBe("closed");
});
});
Loading