import { GITHUB_CHECK_RUN_SCHEMA } from "@automate.ax/integration-contracts/github" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getGitHubApi } from "../lib/api" import { GITHUB_REPOSITORY_INPUT_SCHEMA } from "../lib/input-schemas" import { GITHUB_CHECKS_WRITE_SCOPE } from "../lib/scopes" const GITHUB_CHECK_CONCLUSION_SCHEMA = z.enum([ "action_required", "cancelled", "failure", "neutral", "skipped", "success", "timed_out", ]) const GITHUB_CHECK_ANNOTATION_SCHEMA = z .object({ annotationLevel: z.enum(["failure", "notice", "warning"]), endColumn: z.number().int().positive().optional(), endLine: z.number().int().positive(), message: z.string().min(1).max(65_535), path: z.string().min(1), rawDetails: z.string().max(65_535).optional(), startColumn: z.number().int().positive().optional(), startLine: z.number().int().positive(), title: z.string().max(255).optional(), }) .superRefine((annotation, context) => { const hasStartColumn = annotation.startColumn !== undefined if (hasStartColumn !== (annotation.endColumn !== undefined)) { context.addIssue({ code: "custom", message: "Check annotation columns must be provided together.", }) } if (hasStartColumn && annotation.startLine !== annotation.endLine) { context.addIssue({ code: "custom", message: "Check annotation columns require a single-line range.", }) } }) const GITHUB_CHECK_OUTPUT_BASE_SCHEMA = z.object({ annotations: GITHUB_CHECK_ANNOTATION_SCHEMA.array().max(50).optional(), images: z .object({ alt: z.string().min(1), caption: z.string().optional(), imageUrl: z.url(), }) .array() .optional(), summary: z.string().max(65_535), text: z.string().max(65_535).optional(), }) const GITHUB_CHECK_CREATE_OUTPUT_SCHEMA = GITHUB_CHECK_OUTPUT_BASE_SCHEMA.extend({ title: z.string().max(255) }) const GITHUB_CHECK_UPDATE_OUTPUT_SCHEMA = GITHUB_CHECK_OUTPUT_BASE_SCHEMA.extend({ title: z.string().max(255).optional(), }) const GITHUB_CHECK_ACTION_SCHEMA = z.object({ description: z.string().min(1).max(40), identifier: z.string().min(1).max(20), label: z.string().min(1).max(20), }) const GITHUB_CHECK_LIFECYCLE_INPUT_SCHEMA = z.union([ z.object({ completedAt: z.iso.datetime({ offset: true }).optional(), conclusion: GITHUB_CHECK_CONCLUSION_SCHEMA, status: z.literal("completed"), }), z.object({ completedAt: z.undefined().optional(), conclusion: z.undefined().optional(), status: z.enum(["in_progress", "queued"]).prefault("queued"), }), ]) const GITHUB_CHECK_MUTABLE_FIELDS_SCHEMA = z.object({ /** Up to three actions shown on the check run. */ actions: GITHUB_CHECK_ACTION_SCHEMA.array().max(3).optional(), /** URL with details about the check. */ detailsUrl: z.url().optional(), /** Provider-independent correlation identifier. */ externalId: z.string().optional(), /** Check run display name. */ name: z.string().min(1).max(100).optional(), /** Timestamp when execution began. */ startedAt: z.iso.datetime({ offset: true }).optional(), }) /** Creates a GitHub check run for one commit. */ export const createGitHubCheckRun = defineAction("Create GitHub check run") .describe("Creates a check run for one commit SHA.") .account("github", GITHUB_CHECKS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.and( GITHUB_CHECK_MUTABLE_FIELDS_SCHEMA.extend({ /** Commit SHA being checked. */ headSha: z.string().min(1), /** Check run display name. */ name: z.string().min(1).max(100), /** Rich check output, annotations, and images. */ output: GITHUB_CHECK_CREATE_OUTPUT_SCHEMA.optional(), }).and(GITHUB_CHECK_LIFECYCLE_INPUT_SCHEMA), ), ) .output(GITHUB_CHECK_RUN_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.checks.create({ head_sha: input.headSha, name: input.name, owner: input.owner, repo: input.repository, status: input.status, ...(input.actions && { actions: toGitHubCheckActions(input.actions) }), ...(input.completedAt && { completed_at: input.completedAt }), ...(input.conclusion && { conclusion: input.conclusion }), ...(input.detailsUrl && { details_url: input.detailsUrl }), ...(input.externalId !== undefined && { external_id: input.externalId }), ...(input.output && { output: { ...toGitHubCheckOutput(input.output), title: input.output.title, }, }), ...(input.startedAt && { started_at: input.startedAt }), }) return data }) /** Updates a GitHub check run. */ export const updateGitHubCheckRun = defineAction("Update GitHub check run") .describe("Updates the lifecycle or output of one GitHub check run.") .account("github", GITHUB_CHECKS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.and( GITHUB_CHECK_MUTABLE_FIELDS_SCHEMA.extend({ /** Repository-scoped check run ID. */ checkRunId: z.number().int().positive(), /** Rich check output, annotations, and images. */ output: GITHUB_CHECK_UPDATE_OUTPUT_SCHEMA.optional(), }).and( z.union([ z.object({ completedAt: z.iso.datetime({ offset: true }).optional(), conclusion: GITHUB_CHECK_CONCLUSION_SCHEMA, status: z.literal("completed"), }), z.object({ completedAt: z.undefined().optional(), conclusion: z.undefined().optional(), status: z.enum(["in_progress", "queued"]).optional(), }), ]), ), ), ) .output(GITHUB_CHECK_RUN_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.checks.update({ check_run_id: input.checkRunId, owner: input.owner, repo: input.repository, ...(input.actions && { actions: toGitHubCheckActions(input.actions) }), ...(input.completedAt && { completed_at: input.completedAt }), ...(input.conclusion && { conclusion: input.conclusion }), ...(input.detailsUrl && { details_url: input.detailsUrl }), ...(input.externalId !== undefined && { external_id: input.externalId }), ...(input.name && { name: input.name }), ...(input.output && { output: toGitHubCheckOutput(input.output) }), ...(input.startedAt && { started_at: input.startedAt }), ...(input.status && { status: input.status }), }) return data }) /** * Maps public check actions to GitHub's request field names. * * @param actions - Public check actions. */ function toGitHubCheckActions( actions: z.output[], ) { return actions.map((action) => ({ description: action.description, identifier: action.identifier, label: action.label, })) } /** * Maps public check output to GitHub's request field names. * * @param output - Public check output. */ function toGitHubCheckOutput( output: | z.output | z.output, ) { return { summary: output.summary, ...(output.annotations && { annotations: output.annotations.map((annotation) => ({ annotation_level: annotation.annotationLevel, end_line: annotation.endLine, message: annotation.message, path: annotation.path, start_line: annotation.startLine, ...(annotation.endColumn !== undefined && { end_column: annotation.endColumn, }), ...(annotation.rawDetails !== undefined && { raw_details: annotation.rawDetails, }), ...(annotation.startColumn !== undefined && { start_column: annotation.startColumn, }), ...(annotation.title !== undefined && { title: annotation.title }), })), }), ...(output.images && { images: output.images.map((image) => ({ alt: image.alt, image_url: image.imageUrl, ...(image.caption !== undefined && { caption: image.caption }), })), }), ...(output.text !== undefined && { text: output.text }), ...(output.title !== undefined && { title: output.title }), } }