Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279const std = @import("std");const io_mod = @import("../shared/io.zig");
const Allocator = std.mem.Allocator;
pub const Workflow = enum { pull_request, issue,};
pub const Draft = struct { title: []u8, body: []u8,
pub fn deinit(self: Draft, alloc: Allocator) void { alloc.free(self.title); alloc.free(self.body); }};
pub const PublishResult = struct { ok: bool, text: []u8,
pub fn deinit(self: PublishResult, alloc: Allocator) void { alloc.free(self.text); }};
const CompletedProcess = struct { term: std.process.Child.Term, stdout: []u8, stderr: []u8,};
pub fn parseDraft(alloc: Allocator, text: []const u8) !Draft { const trimmed = std.mem.trim(u8, text, " \t\r\n"); if (trimmed.len == 0) return error.InvalidGithubDraft;
const first_break = std.mem.findScalar(u8, trimmed, '\n') orelse { const title = try alloc.dupe(u8, trimmed); errdefer alloc.free(title); const body = try alloc.dupe(u8, ""); return .{ .title = title, .body = body, }; };
const title_slice = std.mem.trim(u8, trimmed[0..first_break], " \t\r\n"); if (title_slice.len == 0) return error.InvalidGithubDraft;
const body_slice = std.mem.trimStart(u8, trimmed[first_break + 1 ..], " \t\r\n"); const title = try alloc.dupe(u8, title_slice); errdefer alloc.free(title); const body = try alloc.dupe(u8, body_slice); return .{ .title = title, .body = body, };}
pub fn publish(alloc: Allocator, workflow: Workflow, draft: Draft) !PublishResult { var argv = try buildPublishArgv(alloc, workflow, draft.title, draft.body); defer argv.deinit(alloc);
const result = runPublishCommand(alloc, argv.items) catch |err| { return publishResultFromRunError(alloc, err); }; return publishResultFromProcess(alloc, result);}
fn buildPublishArgv(alloc: Allocator, workflow: Workflow, title: []const u8, body: []const u8) !std.ArrayList([]const u8) { var argv = std.ArrayList([]const u8).empty; errdefer argv.deinit(alloc);
try argv.append(alloc, "gh"); try argv.append(alloc, switch (workflow) { .pull_request => "pr", .issue => "issue", }); try argv.append(alloc, "create"); try argv.append(alloc, "--title"); try argv.append(alloc, title); try argv.append(alloc, "--body"); try argv.append(alloc, body); return argv;}
fn runPublishCommand(alloc: Allocator, argv: []const []const u8) !CompletedProcess { const result = try std.process.run(alloc, io_mod.getIo(), .{ .argv = argv, }); return .{ .term = result.term, .stdout = result.stdout, .stderr = result.stderr, };}
fn publishResultFromRunError(alloc: Allocator, err: anyerror) !PublishResult { return switch (err) { error.FileNotFound => .{ .ok = false, .text = try alloc.dupe(u8, "gh CLI not found in PATH") }, else => err, };}
fn publishResultFromProcess(alloc: Allocator, result: CompletedProcess) !PublishResult { defer alloc.free(result.stderr);
if (result.term.exited != 0) { defer alloc.free(result.stdout); const stderr_trimmed = std.mem.trim(u8, result.stderr, " \t\r\n"); if (stderr_trimmed.len > 0) { return .{ .ok = false, .text = try alloc.dupe(u8, stderr_trimmed) }; } return .{ .ok = false, .text = try alloc.dupe(u8, "gh command failed") }; }
const stdout_trimmed = std.mem.trim(u8, result.stdout, " \t\r\n"); if (stdout_trimmed.len == 0) { alloc.free(result.stdout); return .{ .ok = true, .text = try alloc.dupe(u8, "created successfully") }; } if (stdout_trimmed.len == result.stdout.len) { return .{ .ok = true, .text = result.stdout }; }
const text = alloc.dupe(u8, stdout_trimmed) catch |err| { alloc.free(result.stdout); return err; }; alloc.free(result.stdout); return .{ .ok = true, .text = text };}
test "parse draft extracts title and body" { const draft = try parseDraft(std.testing.allocator, "Title\n\n## Summary\nhello"); defer draft.deinit(std.testing.allocator);
try std.testing.expectEqualStrings("Title", draft.title); try std.testing.expectEqualStrings("## Summary\nhello", draft.body);}
test "parse draft rejects whitespace-only input" { try std.testing.expectError(error.InvalidGithubDraft, parseDraft(std.testing.allocator, " \n\t "));}
test "parse draft accepts title-only input" { const draft = try parseDraft(std.testing.allocator, "Title only"); defer draft.deinit(std.testing.allocator);
try std.testing.expectEqualStrings("Title only", draft.title); try std.testing.expectEqualStrings("", draft.body);}
test "parse draft trims outer whitespace and preserves body markdown" { const draft = try parseDraft( std.testing.allocator, " \r\n Title with space \r\n\r\n - one\r\n - two\r\n\n", ); defer draft.deinit(std.testing.allocator);
try std.testing.expectEqualStrings("Title with space", draft.title); try std.testing.expectEqualStrings("- one\r\n - two", draft.body);}
test "publish argv maps pull request workflow" { var argv = try buildPublishArgv(std.testing.allocator, .pull_request, "A title", "Body text"); defer argv.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 7), argv.items.len); try std.testing.expectEqualStrings("gh", argv.items[0]); try std.testing.expectEqualStrings("pr", argv.items[1]); try std.testing.expectEqualStrings("create", argv.items[2]); try std.testing.expectEqualStrings("--title", argv.items[3]); try std.testing.expectEqualStrings("A title", argv.items[4]); try std.testing.expectEqualStrings("--body", argv.items[5]); try std.testing.expectEqualStrings("Body text", argv.items[6]);}
test "publish argv maps issue workflow" { var argv = try buildPublishArgv(std.testing.allocator, .issue, "A title", "Body text"); defer argv.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 7), argv.items.len); try std.testing.expectEqualStrings("gh", argv.items[0]); try std.testing.expectEqualStrings("issue", argv.items[1]); try std.testing.expectEqualStrings("create", argv.items[2]); try std.testing.expectEqualStrings("--title", argv.items[3]); try std.testing.expectEqualStrings("A title", argv.items[4]); try std.testing.expectEqualStrings("--body", argv.items[5]); try std.testing.expectEqualStrings("Body text", argv.items[6]);}
test "publish maps missing gh executable to handled failure" { const result = try publishResultFromRunError(std.testing.allocator, error.FileNotFound); defer result.deinit(std.testing.allocator);
try std.testing.expect(!result.ok); try std.testing.expectEqualStrings("gh CLI not found in PATH", result.text);}
test "publish returns trimmed stderr for non-zero exit" { const stdout = try std.testing.allocator.dupe(u8, "ignored"); const stderr = try std.testing.allocator.dupe(u8, " \nfailed to create\n ");
const result = try publishResultFromProcess(std.testing.allocator, .{ .term = .{ .exited = 1 }, .stdout = stdout, .stderr = stderr, }); defer result.deinit(std.testing.allocator);
try std.testing.expect(!result.ok); try std.testing.expectEqualStrings("failed to create", result.text);}
test "publish returns fallback for non-zero exit without stderr" { const stdout = try std.testing.allocator.dupe(u8, "ignored"); const stderr = try std.testing.allocator.dupe(u8, " \n\t ");
const result = try publishResultFromProcess(std.testing.allocator, .{ .term = .{ .exited = 1 }, .stdout = stdout, .stderr = stderr, }); defer result.deinit(std.testing.allocator);
try std.testing.expect(!result.ok); try std.testing.expectEqualStrings("gh command failed", result.text);}
test "publish returns success fallback for empty stdout" { const stdout = try std.testing.allocator.dupe(u8, " \n\t "); const stderr = try std.testing.allocator.dupe(u8, "");
const result = try publishResultFromProcess(std.testing.allocator, .{ .term = .{ .exited = 0 }, .stdout = stdout, .stderr = stderr, }); defer result.deinit(std.testing.allocator);
try std.testing.expect(result.ok); try std.testing.expectEqualStrings("created successfully", result.text);}
test "publish trims padded stdout" { const stdout = try std.testing.allocator.dupe(u8, " \nhttps://github.com/vercel-labs/fx/pull/1\n "); const stderr = try std.testing.allocator.dupe(u8, "");
const result = try publishResultFromProcess(std.testing.allocator, .{ .term = .{ .exited = 0 }, .stdout = stdout, .stderr = stderr, }); defer result.deinit(std.testing.allocator);
try std.testing.expect(result.ok); try std.testing.expectEqualStrings("https://github.com/vercel-labs/fx/pull/1", result.text);}
test "publish transfers already-trimmed stdout ownership" { const stdout = try std.testing.allocator.dupe(u8, "https://github.com/vercel-labs/fx/issues/2"); const stderr = try std.testing.allocator.dupe(u8, "");
const result = try publishResultFromProcess(std.testing.allocator, .{ .term = .{ .exited = 0 }, .stdout = stdout, .stderr = stderr, }); defer result.deinit(std.testing.allocator);
try std.testing.expect(result.ok); try std.testing.expect(result.text.ptr == stdout.ptr); try std.testing.expectEqualStrings("https://github.com/vercel-labs/fx/issues/2", result.text);}