diff --git a/.changeset/git-create-issue-attach.md b/.changeset/git-create-issue-attach.md
new file mode 100644
index 00000000..1db97755
--- /dev/null
+++ b/.changeset/git-create-issue-attach.md
@@ -0,0 +1,20 @@
+---
+"@upstash/box": patch
+"@upstash/box-cli": patch
+---
+
+Add `git.createIssue()` and file attachments for issues and pull requests.
+
+`box.git.createIssue({ title })` opens a GitHub issue from a box. Both
+`createIssue` and `createPR` accept `attach`, a list of image or video files
+relative to the working directory, which are uploaded to the new item. Alt text
+for an image is written as `shot.png#alt text`; a video cannot take alt text.
+Reference an attachment from the body as `` and GitHub
+rewrites it to point at the uploaded asset.
+
+Both responses gain an optional `warning`, set when `gh` exits non-zero but
+still returns a URL, which means the item exists while an attachment is missing,
+or the pull request was already open.
+
+The CLI gains `box git create-issue --title
` and a repeatable
+`--attach ` on both `create-pr` and `create-issue`.
diff --git a/packages/cli/src/__tests__/repl/commands/git.test.ts b/packages/cli/src/__tests__/repl/commands/git.test.ts
index abb1e4fc..99b5caf9 100644
--- a/packages/cli/src/__tests__/repl/commands/git.test.ts
+++ b/packages/cli/src/__tests__/repl/commands/git.test.ts
@@ -12,6 +12,7 @@ describe("handleGit", () => {
commit: vi.fn().mockResolvedValue({ sha: "abc123", message: "fix bug" }),
push: vi.fn().mockResolvedValue(undefined),
createPR: vi.fn().mockResolvedValue({ number: 42, url: "https://github.com/pr/42" }),
+ createIssue: vi.fn().mockResolvedValue({ number: 9, url: "https://github.com/issues/9" }),
// The repository probe is answered the way git answers it: a working
// tree prints "true", and a bare repository prints "false" with the
// same exit code.
@@ -153,6 +154,37 @@ describe("handleGit", () => {
const events = await collectEvents(handleGit(createMockBox() as any, "create-pr"));
expect(events).toContainEqual({ type: "log", message: "Usage: git create-pr " });
});
+
+ it("prints a warning when gh reported one", async () => {
+ const box = createMockBox();
+ box.git.createPR = vi.fn().mockResolvedValue({
+ number: 42,
+ url: "https://github.com/pr/42",
+ warning: "failed to upload later.png",
+ });
+ const events = await collectEvents(handleGit(box as any, "create-pr Fix"));
+ expect(events).toContainEqual({
+ type: "log",
+ message: "Warning: failed to upload later.png",
+ });
+ });
+ });
+
+ describe("create-issue", () => {
+ it("creates an issue and prints details", async () => {
+ const box = createMockBox();
+ const events = await collectEvents(handleGit(box as any, "create-issue Broken search"));
+ expect(box.git.createIssue).toHaveBeenCalledWith({ title: "Broken search" });
+ expect(events).toContainEqual({
+ type: "log",
+ message: "Issue #9: https://github.com/issues/9",
+ });
+ });
+
+ it("prints usage without title", async () => {
+ const events = await collectEvents(handleGit(createMockBox() as any, "create-issue"));
+ expect(events).toContainEqual({ type: "log", message: "Usage: git create-issue " });
+ });
});
describe("exec", () => {
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index 4b7f6973..685638e7 100644
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -88,6 +88,7 @@ import {
gitCommitCommand,
gitCheckoutCommand,
gitPushCommand,
+ gitCreateIssueCommand,
gitCreatePrCommand,
gitConfigCommand,
gitExecCommand,
@@ -348,6 +349,13 @@ withGitCommon(
await runCommand(async () => gitCheckoutCommand(branch, { ...globals(flags), ...flags }));
});
+// --attach repeats, so each occurrence appends instead of overwriting.
+const attachHelp =
+ "Image or video to upload; repeat for several. Alt text for an image: 'shot.png#alt text'";
+function collectAttach(value: string, previous: string[]): string[] {
+ return [...previous, value];
+}
+
withGitCommon(git.command("push").description("Push the current branch"))
.option("--branch ", "Branch to push")
.action(async (flags: Record) => {
@@ -358,10 +366,19 @@ withGitCommon(git.command("create-pr").description("Open a pull request"))
.requiredOption("--title ", "Pull request title")
.option("--body ", "Pull request body")
.option("--base ", "Base branch")
+ .option("--attach ", attachHelp, collectAttach, [])
.action(async (flags: Record) => {
await runCommand(async () => gitCreatePrCommand({ ...globals(flags), ...flags }));
});
+withGitCommon(git.command("create-issue").description("Open an issue"))
+ .requiredOption("--title ", "Issue title")
+ .option("--body ", "Issue body")
+ .option("--attach ", attachHelp, collectAttach, [])
+ .action(async (flags: Record) => {
+ await runCommand(async () => gitCreateIssueCommand({ ...globals(flags), ...flags }));
+ });
+
withGitCommon(git.command("config").description("Show or set the git identity"))
.option("--name ", "git user.name")
.option("--email ", "git user.email")
diff --git a/packages/cli/src/commands/completion.ts b/packages/cli/src/commands/completion.ts
index 47b9a22d..ab550f31 100644
--- a/packages/cli/src/commands/completion.ts
+++ b/packages/cli/src/commands/completion.ts
@@ -10,7 +10,7 @@ const COMMANDS: [name: string, description: string, subcommands: string][] = [
[
"git",
"Git operations inside the box",
- "clone status diff commit checkout push create-pr config exec",
+ "clone status diff commit checkout push create-pr create-issue config exec",
],
["public-url", "Public URLs for ports inside the box", "list delete"],
["run", "Run the box's agent on a prompt", ""],
diff --git a/packages/cli/src/commands/git.ts b/packages/cli/src/commands/git.ts
index 8872894d..237f387c 100644
--- a/packages/cli/src/commands/git.ts
+++ b/packages/cli/src/commands/git.ts
@@ -15,6 +15,7 @@ export type GitFlags = GlobalFlags & {
title?: string;
body?: string;
base?: string;
+ attach?: string[];
name?: string;
email?: string;
};
@@ -215,8 +216,33 @@ export async function gitCreatePrCommand(flags: GitFlags): Promise {
title: flags.title,
...(flags.body === undefined ? {} : { body: flags.body }),
...(flags.base === undefined ? {} : { base: flags.base }),
+ ...(flags.attach?.length ? { attach: flags.attach } : {}),
});
- emit(pr, pr.url ? `Pull request: ${pr.url}` : "Pull request created", flags);
+ emit(pr, prMessage("Pull request", pr), flags);
+}
+
+/** Open an issue. */
+export async function gitCreateIssueCommand(flags: GitFlags): Promise {
+ if (!flags.title) throw new CliError("Usage: box git create-issue --title ");
+ const box = await open(flags);
+ const issue = await box.git.createIssue({
+ title: flags.title,
+ ...(flags.body === undefined ? {} : { body: flags.body }),
+ ...(flags.attach?.length ? { attach: flags.attach } : {}),
+ });
+ emit(issue, prMessage("Issue", issue), flags);
+}
+
+/**
+ * Text line for a created pull request or issue.
+ *
+ * `warning` is set when gh exited non-zero but still returned a URL, so the
+ * item exists while an attachment may be missing. Text output would otherwise
+ * read as a clean success; JSON output carries the field either way.
+ */
+function prMessage(kind: string, item: { url?: string; warning?: string }): string {
+ const line = item.url ? `${kind}: ${item.url}` : `${kind} created`;
+ return item.warning ? `${line}\nWarning: ${item.warning}` : line;
}
/**
diff --git a/packages/cli/src/repl/client.ts b/packages/cli/src/repl/client.ts
index f296d360..c0ace68d 100644
--- a/packages/cli/src/repl/client.ts
+++ b/packages/cli/src/repl/client.ts
@@ -30,7 +30,7 @@ const COMMANDS: Record> = {
},
git: {
description:
- "Git operations (clone, diff, status, commit, push, create-pr, exec, checkout, config)",
+ "Git operations (clone, diff, status, commit, push, create-pr, create-issue, exec, checkout, config)",
handler: handleGit,
},
snapshot: {
diff --git a/packages/cli/src/repl/commands/git.ts b/packages/cli/src/repl/commands/git.ts
index f326b255..a3e089b8 100644
--- a/packages/cli/src/repl/commands/git.ts
+++ b/packages/cli/src/repl/commands/git.ts
@@ -20,7 +20,7 @@ async function emptyGitMessage(box: Box, clean: string): Promise {
}
/**
- * Handle git subcommands: clone, diff, status, commit, push, create-pr, exec, checkout.
+ * Handle git subcommands: clone, diff, status, commit, push, create-pr, create-issue, exec, checkout.
*/
export async function* handleGit(box: Box, args: string): AsyncGenerator {
const parts = args.split(/\s+/);
@@ -72,6 +72,18 @@ export async function* handleGit(box: Box, args: string): AsyncGenerator" };
+ return;
+ }
+ const issue = await box.git.createIssue({ title });
+ yield { type: "log", message: `Issue #${issue.number}: ${issue.url}` };
+ if (issue.warning) yield { type: "log", message: `Warning: ${issue.warning}` };
break;
}
case "exec": {
@@ -143,7 +155,7 @@ export async function* handleGit(box: Box, args: string): AsyncGenerator [args...]",
+ "Usage: git [args...]",
};
}
}
diff --git a/packages/python-sdk/CHANGELOG.md b/packages/python-sdk/CHANGELOG.md
index e07b62fa..3740dfec 100644
--- a/packages/python-sdk/CHANGELOG.md
+++ b/packages/python-sdk/CHANGELOG.md
@@ -2,6 +2,17 @@
All notable changes to `upstash-box` (Python) are documented here.
+## Unreleased
+
+- Add `git.create_issue()`, which opens a GitHub issue from a box.
+- Add `attach` to `git.create_pr()` and `git.create_issue()`. It takes image or
+ video files, relative to the working directory, and uploads them to the new
+ item. Alt text for an image is written as `shot.png#alt text`; a video cannot
+ take alt text.
+- Add `warning` to `PullRequest` and the new `Issue` model. It is set when `gh`
+ exits non-zero but still returns a URL, which means the item exists while an
+ attachment is missing, or the pull request was already open.
+
## 0.3.1
- Fix `git.update_config()` sending its request to `/v2/box/{id}/git-config`,
diff --git a/packages/python-sdk/PARITY.md b/packages/python-sdk/PARITY.md
index 614eaeef..add19391 100644
--- a/packages/python-sdk/PARITY.md
+++ b/packages/python-sdk/PARITY.md
@@ -28,7 +28,7 @@ JS `Run`/`StreamRun` → Python `Run`/`StreamRun` (+ `AsyncRun`/`AsyncStreamRun`
| `files.read/write/list/upload/download` | `files.read/write/list/upload/download` |
| `files.stat/mkdir/rename/remove` | `files.stat/mkdir/rename/remove` |
| `exec.session` (live WebSocket session) | `exec.session` |
-| `git.clone/diff/status/commit/updateConfig/push/createPR/exec/checkout` | `git.clone/diff/status/commit/update_config/push/create_pr/exec/checkout` |
+| `git.clone/diff/status/commit/updateConfig/push/createPR/createIssue/exec/checkout` | `git.clone/diff/status/commit/update_config/push/create_pr/create_issue/exec/checkout` |
| `schedule.exec/agent/list/get/update/pause/resume/delete` | same (snake) |
| `skills.add/remove/list` | `skills.add/remove/list` |
| `labels.add/remove/list` | `labels.add/remove/list` |
diff --git a/packages/python-sdk/tests/_async/test_box_git.py b/packages/python-sdk/tests/_async/test_box_git.py
index 1821843f..63092d92 100644
--- a/packages/python-sdk/tests/_async/test_box_git.py
+++ b/packages/python-sdk/tests/_async/test_box_git.py
@@ -112,3 +112,50 @@ async def test_push_and_create_pr_and_exec_and_checkout():
assert await box.git.exec(args=["log", "--oneline"]) == "log"
await box.git.checkout(branch="feature")
await box.aclose()
+
+
+@respx.mock
+async def test_create_pr_and_issue_attachments():
+ box = await make_async_box(respx.mock)
+ pr_route = respx.post(f"{BASE}/git/create-pr").mock(
+ return_value=httpx.Response(
+ 200,
+ json={
+ "url": "u",
+ "number": 5,
+ "title": "t",
+ "base": "main",
+ "warning": "failed to upload later.png",
+ },
+ )
+ )
+ issue_route = respx.post(f"{BASE}/git/create-issue").mock(
+ return_value=httpx.Response(200, json={"url": "iu", "number": 9, "title": "bug"})
+ )
+
+ pr = await box.git.create_pr(title="t", attach=["shot.png#the login error"])
+ assert pr.warning == "failed to upload later.png"
+ assert last_json_body(pr_route)["attach"] == ["shot.png#the login error"]
+
+ issue = await box.git.create_issue(title="bug", body="steps", attach=["repro.png"])
+ assert issue.number == 9
+ assert issue.warning is None
+ body = last_json_body(issue_route)
+ assert body["title"] == "bug"
+ assert body["body"] == "steps"
+ assert body["attach"] == ["repro.png"]
+
+ await box.aclose()
+
+
+@respx.mock
+async def test_create_pr_omits_empty_attach():
+ box = await make_async_box(respx.mock)
+ route = respx.post(f"{BASE}/git/create-pr").mock(
+ return_value=httpx.Response(
+ 200, json={"url": "u", "number": 5, "title": "t", "base": "main"}
+ )
+ )
+ await box.git.create_pr(title="t", attach=[])
+ assert "attach" not in last_json_body(route)
+ await box.aclose()
diff --git a/packages/python-sdk/upstash_box/__init__.py b/packages/python-sdk/upstash_box/__init__.py
index 29e518e9..06e24bf0 100644
--- a/packages/python-sdk/upstash_box/__init__.py
+++ b/packages/python-sdk/upstash_box/__init__.py
@@ -88,6 +88,7 @@
GitCommitResult,
GitConfigInput,
GitConfigResult,
+ Issue,
ListOptions,
LogEntry,
McpServerConfig,
@@ -228,6 +229,7 @@
"GitConfigResult",
"LogEntry",
"PublicURL",
+ "Issue",
"PullRequest",
"RunCost",
"RunLog",
diff --git a/packages/python-sdk/upstash_box/_async/client.py b/packages/python-sdk/upstash_box/_async/client.py
index 8319060c..9d640412 100644
--- a/packages/python-sdk/upstash_box/_async/client.py
+++ b/packages/python-sdk/upstash_box/_async/client.py
@@ -68,6 +68,7 @@
FinishUsage,
GitCommitResult,
GitConfigResult,
+ Issue,
ListOptions,
LogEntry,
ModelConfig,
@@ -429,9 +430,23 @@ async def push(self, *, branch: Optional[str] = None) -> None:
await self._box._git_push(branch)
async def create_pr(
- self, *, title: str, body: Optional[str] = None, base: Optional[str] = None
+ self,
+ *,
+ title: str,
+ body: Optional[str] = None,
+ base: Optional[str] = None,
+ attach: Optional[List[str]] = None,
) -> PullRequest:
- return await self._box._git_create_pr(title, body, base)
+ return await self._box._git_create_pr(title, body, base, attach)
+
+ async def create_issue(
+ self,
+ *,
+ title: str,
+ body: Optional[str] = None,
+ attach: Optional[List[str]] = None,
+ ) -> Issue:
+ return await self._box._git_create_issue(title, body, attach)
async def exec(self, *, args: List[str]) -> str:
return await self._box._git_exec(args)
@@ -1713,14 +1728,26 @@ async def _git_push(self, branch) -> None:
body["folder"] = folder
await self._request("POST", f"/v2/box/{self.id}/git/push", body=body)
- async def _git_create_pr(self, title, body_text, base) -> PullRequest:
+ async def _git_create_pr(self, title, body_text, base, attach=None) -> PullRequest:
folder = self._get_folder()
body: Dict[str, Any] = {"title": title, "body": body_text, "base": base}
+ if attach:
+ body["attach"] = attach
if folder:
body["folder"] = folder
data = await self._request("POST", f"/v2/box/{self.id}/git/create-pr", body=body)
return PullRequest.model_validate(data)
+ async def _git_create_issue(self, title, body_text, attach=None) -> Issue:
+ folder = self._get_folder()
+ body: Dict[str, Any] = {"title": title, "body": body_text}
+ if attach:
+ body["attach"] = attach
+ if folder:
+ body["folder"] = folder
+ data = await self._request("POST", f"/v2/box/{self.id}/git/create-issue", body=body)
+ return Issue.model_validate(data)
+
async def _git_exec(self, args) -> str:
folder = self._get_folder()
body: Dict[str, Any] = {"args": args}
diff --git a/packages/python-sdk/upstash_box/_sync/client.py b/packages/python-sdk/upstash_box/_sync/client.py
index 9c49635e..5f9a5878 100644
--- a/packages/python-sdk/upstash_box/_sync/client.py
+++ b/packages/python-sdk/upstash_box/_sync/client.py
@@ -67,6 +67,7 @@
FinishUsage,
GitCommitResult,
GitConfigResult,
+ Issue,
ListOptions,
LogEntry,
ModelConfig,
@@ -424,9 +425,23 @@ def push(self, *, branch: Optional[str] = None) -> None:
self._box._git_push(branch)
def create_pr(
- self, *, title: str, body: Optional[str] = None, base: Optional[str] = None
+ self,
+ *,
+ title: str,
+ body: Optional[str] = None,
+ base: Optional[str] = None,
+ attach: Optional[List[str]] = None,
) -> PullRequest:
- return self._box._git_create_pr(title, body, base)
+ return self._box._git_create_pr(title, body, base, attach)
+
+ def create_issue(
+ self,
+ *,
+ title: str,
+ body: Optional[str] = None,
+ attach: Optional[List[str]] = None,
+ ) -> Issue:
+ return self._box._git_create_issue(title, body, attach)
def exec(self, *, args: List[str]) -> str:
return self._box._git_exec(args)
@@ -1698,14 +1713,26 @@ def _git_push(self, branch) -> None:
body["folder"] = folder
self._request("POST", f"/v2/box/{self.id}/git/push", body=body)
- def _git_create_pr(self, title, body_text, base) -> PullRequest:
+ def _git_create_pr(self, title, body_text, base, attach=None) -> PullRequest:
folder = self._get_folder()
body: Dict[str, Any] = {"title": title, "body": body_text, "base": base}
+ if attach:
+ body["attach"] = attach
if folder:
body["folder"] = folder
data = self._request("POST", f"/v2/box/{self.id}/git/create-pr", body=body)
return PullRequest.model_validate(data)
+ def _git_create_issue(self, title, body_text, attach=None) -> Issue:
+ folder = self._get_folder()
+ body: Dict[str, Any] = {"title": title, "body": body_text}
+ if attach:
+ body["attach"] = attach
+ if folder:
+ body["folder"] = folder
+ data = self._request("POST", f"/v2/box/{self.id}/git/create-issue", body=body)
+ return Issue.model_validate(data)
+
def _git_exec(self, args) -> str:
folder = self._get_folder()
body: Dict[str, Any] = {"args": args}
diff --git a/packages/python-sdk/upstash_box/types.py b/packages/python-sdk/upstash_box/types.py
index 63073741..277afe27 100644
--- a/packages/python-sdk/upstash_box/types.py
+++ b/packages/python-sdk/upstash_box/types.py
@@ -556,6 +556,18 @@ class PullRequest(_Model):
number: int
title: str
base: str
+ # Set when gh exited non-zero but still returned a URL: the pull request
+ # exists, but an attachment failed to upload or it was already open.
+ warning: Optional[str] = None
+
+
+class Issue(_Model):
+ url: str
+ number: int
+ title: str
+ # Set when gh exited non-zero but still returned a URL, which means an
+ # attachment failed to upload.
+ warning: Optional[str] = None
class Snapshot(_Model):
diff --git a/packages/sdk/src/__tests__/box-git.test.ts b/packages/sdk/src/__tests__/box-git.test.ts
index 2260e6b0..cf90ddd6 100644
--- a/packages/sdk/src/__tests__/box-git.test.ts
+++ b/packages/sdk/src/__tests__/box-git.test.ts
@@ -194,6 +194,74 @@ describe("Box git operations", () => {
expect(body.title).toBe("Fix");
expect(body.body).toBe("desc");
expect(body.base).toBe("main");
+ expect(body.attach).toBeUndefined();
+ });
+
+ it("sends attachments and surfaces a warning", async () => {
+ const { box, fetchMock } = await createTestBox();
+ fetchMock.mockResolvedValueOnce(
+ mockResponse({
+ url: "https://github.com/owner/repo/pull/42",
+ number: 42,
+ title: "Fix",
+ base: "main",
+ warning: "failed to upload later.png",
+ }),
+ );
+
+ const pr = await box.git.createPR({
+ title: "Fix",
+ attach: ["shot.png#the login error", "clip.mp4"],
+ });
+ expect(pr.warning).toBe("failed to upload later.png");
+
+ const body = JSON.parse(fetchMock.mock.calls[1]![1]?.body as string);
+ expect(body.attach).toEqual(["shot.png#the login error", "clip.mp4"]);
+ });
+
+ it("omits an empty attach list", async () => {
+ const { box, fetchMock } = await createTestBox();
+ fetchMock.mockResolvedValueOnce(
+ mockResponse({ url: "u", number: 1, title: "t", base: "main" }),
+ );
+
+ await box.git.createPR({ title: "Fix", attach: [] });
+ const body = JSON.parse(fetchMock.mock.calls[1]![1]?.body as string);
+ expect(body.attach).toBeUndefined();
+ });
+ });
+
+ describe("git.createIssue", () => {
+ it("creates an issue", async () => {
+ const { box, fetchMock } = await createTestBox();
+ fetchMock.mockResolvedValueOnce(
+ mockResponse({
+ url: "https://github.com/owner/repo/issues/9",
+ number: 9,
+ title: "Bug",
+ }),
+ );
+
+ const issue = await box.git.createIssue({ title: "Bug", body: "steps" });
+ expect(issue.number).toBe(9);
+ expect(issue.url).toContain("issues/9");
+ expect(issue.warning).toBeUndefined();
+
+ const call = fetchMock.mock.calls[1]!;
+ expect(call[0]).toContain("/git/create-issue");
+ const body = JSON.parse(call[1]?.body as string);
+ expect(body.title).toBe("Bug");
+ expect(body.body).toBe("steps");
+ expect(body.base).toBeUndefined();
+ });
+
+ it("sends attachments", async () => {
+ const { box, fetchMock } = await createTestBox();
+ fetchMock.mockResolvedValueOnce(mockResponse({ url: "u", number: 9, title: "Bug" }));
+
+ await box.git.createIssue({ title: "Bug", attach: ["repro.png#the repro"] });
+ const body = JSON.parse(fetchMock.mock.calls[1]![1]?.body as string);
+ expect(body.attach).toEqual(["repro.png#the repro"]);
});
});
diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts
index 0af4109a..22d08b39 100644
--- a/packages/sdk/src/client.ts
+++ b/packages/sdk/src/client.ts
@@ -28,11 +28,13 @@ import {
type GitExecOptions,
type GitExecResult,
type GitCheckoutOptions,
+ type GitIssueOptions,
type GitPROptions,
type GitCommitOptions,
type GitConfigUpdateOptions,
type GitConfig,
type GitCommitResult,
+ type Issue,
type PullRequest,
type LogEntry,
type UploadFileEntry,
@@ -694,6 +696,7 @@ export class Box {
updateConfig: (options: GitConfigUpdateOptions) => Promise;
push: (options?: { branch?: string }) => Promise;
createPR: (options: GitPROptions) => Promise;
+ createIssue: (options: GitIssueOptions) => Promise;
exec: (options: GitExecOptions) => Promise;
checkout: (options: GitCheckoutOptions) => Promise;
};
@@ -888,6 +891,7 @@ export class Box {
updateConfig: (options) => this._gitUpdateConfig(options),
push: (options) => this._gitPush(options),
createPR: (options) => this._gitCreatePR(options),
+ createIssue: (options) => this._gitCreateIssue(options),
exec: (options) => this._gitExec(options),
checkout: (options) => this._gitCheckout(options),
};
@@ -3042,6 +3046,19 @@ export class Box {
title: options.title,
body: options.body,
base: options.base,
+ ...(options.attach?.length ? { attach: options.attach } : {}),
+ ...(folder ? { folder } : {}),
+ },
+ });
+ }
+
+ private async _gitCreateIssue(options: GitIssueOptions): Promise {
+ const folder = this._getFolder();
+ return this._request("POST", `/v2/box/${this.id}/git/create-issue`, {
+ body: {
+ title: options.title,
+ body: options.body,
+ ...(options.attach?.length ? { attach: options.attach } : {}),
...(folder ? { folder } : {}),
},
});
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index 48369f6d..b1089cbe 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -42,6 +42,7 @@ export type {
RunStatus,
RunCost,
RunLog,
+ Issue,
PullRequest,
WebhookConfig,
WebhookPayload,
@@ -56,6 +57,7 @@ export type {
GitExecOptions,
GitExecResult,
GitCheckoutOptions,
+ GitIssueOptions,
GitPROptions,
GitCommitOptions,
GitConfigUpdateOptions,
diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts
index 798d214a..7ee14c11 100644
--- a/packages/sdk/src/types.ts
+++ b/packages/sdk/src/types.ts
@@ -744,6 +744,26 @@ export interface PullRequest {
number: number;
title: string;
base: string;
+ /**
+ * Set when `gh` exited non-zero but still returned a URL. The pull request
+ * exists, but an attachment failed to upload or the pull request was already
+ * open, so it was not created by this call. Absent on a clean create.
+ */
+ warning?: string;
+}
+
+/**
+ * A GitHub issue created from a box.
+ */
+export interface Issue {
+ url: string;
+ number: number;
+ title: string;
+ /**
+ * Set when `gh` exited non-zero but still returned a URL, which means an
+ * attachment failed to upload. Absent on a clean create.
+ */
+ warning?: string;
}
/**
@@ -1032,6 +1052,23 @@ export interface GitPROptions {
title: string;
body?: string;
base?: string;
+ /**
+ * Image or video files to upload, relative to the box's working directory.
+ * Add alt text for an image with `path#alt text`; a video cannot take alt
+ * text. Reference an attachment from `body` as `` and
+ * GitHub rewrites it to point at the uploaded asset.
+ */
+ attach?: string[];
+}
+
+/**
+ * Options for opening a GitHub issue.
+ */
+export interface GitIssueOptions {
+ title: string;
+ body?: string;
+ /** Image or video files to upload. See {@link GitPROptions.attach}. */
+ attach?: string[];
}
export interface GitCommitOptions {