import * as z from "zod" import { defineAction } from "../../automation/actions" import { getGitHubApi } from "./lib/api" import { GITHUB_ISSUE_COMMENT_SCHEMA, GITHUB_ISSUE_SCHEMA, GITHUB_PULL_REQUEST_MERGE_SCHEMA, GITHUB_PULL_REQUEST_REVIEW_SCHEMA, GITHUB_PULL_REQUEST_SCHEMA, GITHUB_PULL_REQUEST_SUMMARY_SCHEMA, } from "@automate.ax/integration-contracts/github" import { GITHUB_CONTENTS_WRITE_SCOPE, GITHUB_ISSUE_COMMENT_REQUIREMENT, GITHUB_ISSUE_UPDATE_REQUIREMENT, GITHUB_ISSUES_READ_SCOPE, GITHUB_ISSUES_WRITE_SCOPE, GITHUB_PULL_REQUESTS_READ_SCOPE, GITHUB_PULL_REQUESTS_WRITE_SCOPE, } from "./lib/scopes" import { GITHUB_ISSUE_NUMBER_INPUT_SCHEMA, GITHUB_PULL_REQUEST_NUMBER_INPUT_SCHEMA, GITHUB_REPOSITORY_INPUT_SCHEMA, } from "./lib/input-schemas" export * from "./actions/checks" export * from "./actions/deployments" export * from "./actions/issues" export * from "./actions/pull-requests" export * from "./actions/releases" export * from "./actions/repositories" export * from "./actions/workflows" const GITHUB_ISSUE_FIELD_VALUE_SCHEMA = z.object({ /** ID of the issue field to set. */ fieldId: z.number().int(), /** Field value, or option names for a multi-select field. */ value: z.union([z.string(), z.number(), z.string().array()]), }) const GITHUB_ISSUE_CHANGE_CONFIDENCE_SCHEMA = z.enum(["high", "low", "medium"]) const GITHUB_ISSUE_LABEL_OBJECT_SCHEMA = z.object({ color: z.string().nullable().optional(), description: z.string().nullable().optional(), id: z.number().int().optional(), name: z.string().optional(), }) const GITHUB_ISSUE_LABEL_SCHEMA = z.union([ z.string(), GITHUB_ISSUE_LABEL_OBJECT_SCHEMA, ]) const GITHUB_ISSUE_UPDATE_METADATA_SCHEMA = { confidence: GITHUB_ISSUE_CHANGE_CONFIDENCE_SCHEMA.optional(), rationale: z.string().optional(), suggest: z.boolean().optional(), } /** Gets one GitHub issue or pull request by repository-local number. */ export const getGitHubIssue = defineAction("Get GitHub issue") .describe("Gets one issue or pull request from a GitHub repository.") .account("github", GITHUB_ISSUES_READ_SCOPE) .input(GITHUB_ISSUE_NUMBER_INPUT_SCHEMA) .output(GITHUB_ISSUE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.issues.get({ issue_number: input.issueNumber, owner: input.owner, repo: input.repository, }) return data }) /** Lists a page of GitHub issues and pull requests in one repository. */ export const listGitHubIssues = defineAction("List GitHub issues") .describe("Lists a page of issues and pull requests in a GitHub repository.") .account("github", GITHUB_ISSUES_READ_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Filter by an assignee login, `none`, or `*` for any assignee. */ assignee: z.string().optional(), /** Filter by the issue creator's login. */ creator: z.string().optional(), /** Sort direction. */ direction: z.enum(["asc", "desc"]).optional(), /** Labels that must be present on returned issues. */ labels: z.string().array().optional(), /** One-based result page. */ page: z.number().int().positive().prefault(1), /** Results per page. */ perPage: z.number().int().min(1).max(100).prefault(30), /** Include open issues, closed issues, or both. */ state: z.enum(["all", "closed", "open"]).prefault("open"), }), ) .output(GITHUB_ISSUE_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.issues.listForRepo( { owner: input.owner, page: input.page, per_page: input.perPage, repo: input.repository, state: input.state, ...(input.assignee && { assignee: input.assignee }), ...(input.creator && { creator: input.creator }), ...(input.direction && { direction: input.direction }), ...(input.labels?.length && { labels: input.labels.join(",") }), }, ) return data }) /** Creates a GitHub issue. */ export const createGitHubIssue = defineAction("Create GitHub issue") .describe("Creates an issue in a GitHub repository.") .account("github", GITHUB_ISSUES_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** User logins assigned to the issue. */ assignees: z.string().array().optional(), /** Markdown issue body. */ body: z.string().optional(), /** Labels to apply by name or label object. */ labels: GITHUB_ISSUE_LABEL_SCHEMA.array().optional(), /** Custom issue field values. */ issueFieldValues: GITHUB_ISSUE_FIELD_VALUE_SCHEMA.array().optional(), /** Milestone name or number, or `null`. */ milestone: z.union([z.string(), z.number().int()]).nullable().optional(), /** Issue title. */ title: z.union([z.string(), z.number().int()]), /** Issue type name, or `null`. */ type: z.string().nullable().optional(), }), ) .output(GITHUB_ISSUE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).request< z.output >({ method: "POST", owner: input.owner, repo: input.repository, url: "/repos/{owner}/{repo}/issues", title: input.title, ...(input.assignees && { assignees: input.assignees }), ...(input.body !== undefined && { body: input.body }), ...(input.issueFieldValues && { issue_field_values: input.issueFieldValues.map( ({ fieldId, value }) => ({ field_id: fieldId, value }), ), }), ...(input.labels && { labels: input.labels }), ...(input.milestone !== undefined && { milestone: input.milestone }), ...(input.type !== undefined && { type: input.type }), }) return data }) /** Updates a GitHub issue or pull request. */ export const updateGitHubIssue = defineAction("Update GitHub issue") .describe("Updates issue fields in a GitHub repository.") .account("github", GITHUB_ISSUE_UPDATE_REQUIREMENT) .input( GITHUB_ISSUE_NUMBER_INPUT_SCHEMA.extend({ /** Replacement assignees by login or suggested assignee object. */ assignees: z .union([ z.string(), z.object({ login: z.string().optional(), ...GITHUB_ISSUE_UPDATE_METADATA_SCHEMA, }), ]) .array() .optional(), /** Replacement Markdown issue body. */ body: z.string().nullable().optional(), /** Replacement labels by name or label object. */ labels: z .union([ z.string(), GITHUB_ISSUE_LABEL_OBJECT_SCHEMA.extend( GITHUB_ISSUE_UPDATE_METADATA_SCHEMA, ), ]) .array() .optional(), /** Replacement custom issue field values. */ issueFieldValues: GITHUB_ISSUE_FIELD_VALUE_SCHEMA.extend( GITHUB_ISSUE_UPDATE_METADATA_SCHEMA, ) .array() .optional(), /** Replacement milestone name or number, or `null` to clear it. */ milestone: z.union([z.string(), z.number().int()]).nullable().optional(), /** Canonical issue database ID when marking this issue as a duplicate. */ duplicateIssueId: z.number().int().optional(), /** New issue state. */ state: z.enum(["closed", "open"]).optional(), /** Reason for closing or reopening the issue. */ stateReason: z .enum(["completed", "duplicate", "not_planned", "reopened"]) .nullable() .optional(), /** Replacement issue title. */ title: z.union([z.string(), z.number().int()]).nullable().optional(), /** Replacement issue type name, suggestion metadata, or `null`. */ type: z .union([ z.string(), z.object({ value: z.string().nullable().optional(), ...GITHUB_ISSUE_UPDATE_METADATA_SCHEMA, }), ]) .nullable() .optional(), }), ) .output(GITHUB_ISSUE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).request< z.output >({ issue_number: input.issueNumber, method: "PATCH", owner: input.owner, repo: input.repository, url: "/repos/{owner}/{repo}/issues/{issue_number}", ...(input.assignees && { assignees: input.assignees }), ...(input.body !== undefined && { body: input.body }), ...(input.duplicateIssueId !== undefined && { duplicate_issue_id: input.duplicateIssueId, }), ...(input.issueFieldValues && { issue_field_values: input.issueFieldValues.map( ({ fieldId, ...fieldValue }) => ({ field_id: fieldId, ...fieldValue, }), ), }), ...(input.labels && { labels: input.labels }), ...(input.milestone !== undefined && { milestone: input.milestone }), ...(input.state && { state: input.state }), ...(input.stateReason !== undefined && { state_reason: input.stateReason, }), ...(input.title !== undefined && { title: input.title }), ...(input.type !== undefined && { type: input.type }), }) return data }) /** Creates a comment on a GitHub issue or pull request. */ export const createGitHubIssueComment = defineAction( "Create GitHub issue comment", ) .describe("Creates a Markdown comment on a GitHub issue or pull request.") .account("github", GITHUB_ISSUE_COMMENT_REQUIREMENT) .input( GITHUB_ISSUE_NUMBER_INPUT_SCHEMA.extend({ /** Markdown comment body. */ body: z.string().min(1), }), ) .output(GITHUB_ISSUE_COMMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi( account.secret, ).rest.issues.createComment({ body: input.body, issue_number: input.issueNumber, owner: input.owner, repo: input.repository, }) return data }) /** Gets one GitHub pull request. */ export const getGitHubPullRequest = defineAction("Get GitHub pull request") .describe("Gets one pull request from a GitHub repository.") .account("github", GITHUB_PULL_REQUESTS_READ_SCOPE) .input(GITHUB_PULL_REQUEST_NUMBER_INPUT_SCHEMA) .output(GITHUB_PULL_REQUEST_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.pulls.get({ owner: input.owner, pull_number: input.pullRequestNumber, repo: input.repository, }) return data }) /** Lists a page of GitHub pull requests in one repository. */ export const listGitHubPullRequests = defineAction("List GitHub pull requests") .describe("Lists a page of pull requests in a GitHub repository.") .account("github", GITHUB_PULL_REQUESTS_READ_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Filter by target branch. */ base: z.string().optional(), /** Sort direction. */ direction: z.enum(["asc", "desc"]).optional(), /** Filter by source in `owner:branch` format. */ head: z.string().optional(), /** One-based result page. */ page: z.number().int().positive().prefault(1), /** Results per page. */ perPage: z.number().int().min(1).max(100).prefault(30), /** Pull request sort field. */ sort: z .enum(["created", "long-running", "popularity", "updated"]) .optional(), /** Include open pull requests, closed pull requests, or both. */ state: z.enum(["all", "closed", "open"]).prefault("open"), }), ) .output(GITHUB_PULL_REQUEST_SUMMARY_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.pulls.list({ owner: input.owner, page: input.page, per_page: input.perPage, repo: input.repository, state: input.state, ...(input.base && { base: input.base }), ...(input.direction && { direction: input.direction }), ...(input.head && { head: input.head }), ...(input.sort && { sort: input.sort }), }) return data }) /** Creates a GitHub pull request. */ export const createGitHubPullRequest = defineAction( "Create GitHub pull request", ) .describe("Creates a pull request between two branches.") .account("github", GITHUB_PULL_REQUESTS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Target branch for the pull request. */ base: z.string().min(1), /** Markdown pull request body. */ body: z.string().optional(), /** Create the pull request as a draft. */ draft: z.boolean().optional(), /** Source branch, optionally prefixed with its fork owner. */ head: z.string().min(1), /** * Source repository for cross-repository pull requests in one * organization. */ headRepository: z.string().min(1).optional(), /** Allow maintainers to modify the source branch. */ maintainerCanModify: z.boolean().optional(), /** Pull request title. */ title: z.string().min(1), }), ) .output(GITHUB_PULL_REQUEST_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.pulls.create({ base: input.base, head: input.head, owner: input.owner, repo: input.repository, title: input.title, ...(input.body !== undefined && { body: input.body }), ...(input.draft !== undefined && { draft: input.draft }), ...(input.headRepository && { head_repo: input.headRepository }), ...(input.maintainerCanModify !== undefined && { maintainer_can_modify: input.maintainerCanModify, }), }) return data }) /** Updates a GitHub pull request. */ export const updateGitHubPullRequest = defineAction( "Update GitHub pull request", ) .describe("Updates pull request metadata or state.") .account("github", GITHUB_PULL_REQUESTS_WRITE_SCOPE) .input( GITHUB_PULL_REQUEST_NUMBER_INPUT_SCHEMA.extend({ /** Replacement target branch. */ base: z.string().min(1).optional(), /** Replacement Markdown pull request body. */ body: z.string().optional(), /** Allow maintainers to modify the source branch. */ maintainerCanModify: z.boolean().optional(), /** New pull request state. */ state: z.enum(["closed", "open"]).optional(), /** Replacement pull request title. */ title: z.string().min(1).optional(), }), ) .output(GITHUB_PULL_REQUEST_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.pulls.update({ owner: input.owner, pull_number: input.pullRequestNumber, repo: input.repository, ...(input.base && { base: input.base }), ...(input.body !== undefined && { body: input.body }), ...(input.maintainerCanModify !== undefined && { maintainer_can_modify: input.maintainerCanModify, }), ...(input.state && { state: input.state }), ...(input.title && { title: input.title }), }) return data }) /** Merges a GitHub pull request. */ export const mergeGitHubPullRequest = defineAction("Merge GitHub pull request") .describe("Merges a pull request using the repository's allowed methods.") .account("github", GITHUB_CONTENTS_WRITE_SCOPE) .input( GITHUB_PULL_REQUEST_NUMBER_INPUT_SCHEMA.extend({ /** Commit message for the merge commit. */ commitMessage: z.string().optional(), /** Title for the merge commit. */ commitTitle: z.string().optional(), /** Merge strategy. */ mergeMethod: z.enum(["merge", "rebase", "squash"]).optional(), /** Expected pull request head SHA. */ sha: z.string().optional(), }), ) .output(GITHUB_PULL_REQUEST_MERGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.pulls.merge({ owner: input.owner, pull_number: input.pullRequestNumber, repo: input.repository, ...(input.commitMessage && { commit_message: input.commitMessage }), ...(input.commitTitle && { commit_title: input.commitTitle }), ...(input.mergeMethod && { merge_method: input.mergeMethod }), ...(input.sha && { sha: input.sha }), }) return data }) /** Creates a GitHub pull request review. */ export const createGitHubPullRequestReview = defineAction( "Create GitHub pull request review", ) .describe("Creates a summary review on a GitHub pull request.") .account("github", GITHUB_PULL_REQUESTS_WRITE_SCOPE) .input( GITHUB_PULL_REQUEST_NUMBER_INPUT_SCHEMA.and( z .object({ /** Commit SHA to review. Defaults to the current pull request head. */ commitId: z.string().optional(), }) .and( z.discriminatedUnion("event", [ z.object({ /** Optional Markdown approval body. */ body: z.string().optional(), event: z.literal("APPROVE"), }), z.object({ /** Markdown review comment. */ body: z.string().min(1), event: z.literal("COMMENT"), }), z.object({ /** Markdown explanation for the requested changes. */ body: z.string().min(1), event: z.literal("REQUEST_CHANGES"), }), ]), ), ), ) .output(GITHUB_PULL_REQUEST_REVIEW_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.pulls.createReview( { event: input.event, owner: input.owner, pull_number: input.pullRequestNumber, repo: input.repository, ...(input.body !== undefined && { body: input.body }), ...(input.commitId && { commit_id: input.commitId }), }, ) return data })