import { GITHUB_EMPTY_RESPONSE_SCHEMA, GITHUB_RELEASE_ASSET_SCHEMA, GITHUB_RELEASE_SCHEMA, } from "@automate.ax/integration-contracts/github" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getGitHubApi } from "../lib/api" import { GITHUB_PAGE_INPUT_SCHEMA, GITHUB_REPOSITORY_INPUT_SCHEMA, } from "../lib/input-schemas" import { GITHUB_CONTENTS_READ_SCOPE, GITHUB_CONTENTS_WRITE_SCOPE, } from "../lib/scopes" const GITHUB_MAKE_LATEST_SCHEMA = z.enum(["false", "legacy", "true"]) /** Lists releases in one GitHub repository. */ export const listGitHubReleases = defineAction("List GitHub releases") .describe("Lists a page of releases in a GitHub repository.") .account("github", GITHUB_CONTENTS_READ_SCOPE) .input(GITHUB_REPOSITORY_INPUT_SCHEMA.extend(GITHUB_PAGE_INPUT_SCHEMA)) .output(GITHUB_RELEASE_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.repos.listReleases( { owner: input.owner, page: input.page, per_page: input.perPage, repo: input.repository, }, ) return data }) /** Gets one GitHub release by ID. */ export const getGitHubRelease = defineAction("Get GitHub release") .describe("Gets one GitHub release by repository release ID.") .account("github", GITHUB_CONTENTS_READ_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Repository-scoped release ID. */ releaseId: z.number().int().positive(), }), ) .output(GITHUB_RELEASE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi(account.secret).rest.repos.getRelease({ owner: input.owner, release_id: input.releaseId, repo: input.repository, }) return data }) /** Creates a GitHub release. */ export const createGitHubRelease = defineAction("Create GitHub release") .describe("Creates a draft, prerelease, or published GitHub release.") .account("github", GITHUB_CONTENTS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Markdown release notes. */ body: z.string().optional(), /** Create the release as a draft. */ draft: z.boolean().prefault(false), /** Generate release notes from repository history. */ generateReleaseNotes: z.boolean().prefault(false), /** Category slug for an associated discussion. */ discussionCategoryName: z.string().min(1).optional(), /** Whether GitHub should mark this release as latest. */ makeLatest: GITHUB_MAKE_LATEST_SCHEMA.optional(), /** Release display name. */ name: z.string().optional(), /** Mark the release as a prerelease. */ prerelease: z.boolean().prefault(false), /** Tag name for the release. */ tagName: z.string().min(1), /** Commitish used when GitHub must create the tag. */ targetCommitish: z.string().min(1).optional(), }), ) .output(GITHUB_RELEASE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi( account.secret, ).rest.repos.createRelease({ draft: input.draft, generate_release_notes: input.generateReleaseNotes, owner: input.owner, prerelease: input.prerelease, repo: input.repository, tag_name: input.tagName, ...(input.body !== undefined && { body: input.body }), ...(input.discussionCategoryName && { discussion_category_name: input.discussionCategoryName, }), ...(input.makeLatest && { make_latest: input.makeLatest }), ...(input.name !== undefined && { name: input.name }), ...(input.targetCommitish && { target_commitish: input.targetCommitish, }), }) return data }) /** Updates a GitHub release. */ export const updateGitHubRelease = defineAction("Update GitHub release") .describe("Updates a GitHub release's tag, notes, or publication state.") .account("github", GITHUB_CONTENTS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Replacement Markdown release notes. */ body: z.string().optional(), /** Replacement draft state. */ draft: z.boolean().optional(), /** Replacement category slug for an associated discussion. */ discussionCategoryName: z.string().min(1).optional(), /** Whether GitHub should mark this release as latest. */ makeLatest: GITHUB_MAKE_LATEST_SCHEMA.optional(), /** Replacement release display name. */ name: z.string().optional(), /** Replacement prerelease state. */ prerelease: z.boolean().optional(), /** Repository-scoped release ID. */ releaseId: z.number().int().positive(), /** Replacement tag name. */ tagName: z.string().min(1).optional(), /** Replacement commitish used when GitHub must create the tag. */ targetCommitish: z.string().min(1).optional(), }), ) .output(GITHUB_RELEASE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi( account.secret, ).rest.repos.updateRelease({ owner: input.owner, release_id: input.releaseId, repo: input.repository, ...(input.body !== undefined && { body: input.body }), ...(input.discussionCategoryName && { discussion_category_name: input.discussionCategoryName, }), ...(input.draft !== undefined && { draft: input.draft }), ...(input.makeLatest && { make_latest: input.makeLatest }), ...(input.name !== undefined && { name: input.name }), ...(input.prerelease !== undefined && { prerelease: input.prerelease, }), ...(input.tagName && { tag_name: input.tagName }), ...(input.targetCommitish && { target_commitish: input.targetCommitish, }), }) return data }) /** Deletes a GitHub release without deleting its Git tag. */ export const deleteGitHubRelease = defineAction("Delete GitHub release") .describe("Deletes a release while leaving its Git tag unchanged.") .account("github", GITHUB_CONTENTS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Repository-scoped release ID. */ releaseId: z.number().int().positive(), }), ) .output(GITHUB_EMPTY_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getGitHubApi(account.secret).rest.repos.deleteRelease({ owner: input.owner, release_id: input.releaseId, repo: input.repository, }) return {} }) /** Lists assets attached to one GitHub release. */ export const listGitHubReleaseAssets = defineAction( "List GitHub release assets", ) .describe("Lists a page of assets attached to one GitHub release.") .account("github", GITHUB_CONTENTS_READ_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ ...GITHUB_PAGE_INPUT_SCHEMA, /** Repository-scoped release ID. */ releaseId: z.number().int().positive(), }), ) .output(GITHUB_RELEASE_ASSET_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi( account.secret, ).rest.repos.listReleaseAssets({ owner: input.owner, page: input.page, per_page: input.perPage, release_id: input.releaseId, repo: input.repository, }) return data }) /** Uploads one binary asset to a GitHub release. */ export const uploadGitHubReleaseAsset = defineAction( "Upload GitHub release asset", ) .describe("Uploads Base64-encoded binary content to one GitHub release.") .account("github", GITHUB_CONTENTS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Asset bytes encoded as Base64. */ dataBase64: z.base64(), /** Asset media type. */ contentType: z.string().min(1), /** Optional asset display label. */ label: z.string().optional(), /** File name shown on the release. */ name: z.string().min(1), /** Repository-scoped release ID. */ releaseId: z.number().int().positive(), }), ) .output(GITHUB_RELEASE_ASSET_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const data = Uint8Array.fromBase64(input.dataBase64) const url = new URL( `https://uploads.github.com/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repository)}/releases/${input.releaseId}/assets`, ) url.searchParams.set("name", input.name) if (input.label !== undefined) url.searchParams.set("label", input.label) const { data: asset } = await getGitHubApi(account.secret).request< z.output >({ data, headers: { "content-length": String(data.byteLength), "content-type": input.contentType, }, method: "POST", url: url.href, }) return asset }) /** Renames or relabels one asset attached to a GitHub release. */ export const updateGitHubReleaseAsset = defineAction( "Update GitHub release asset", ) .describe("Renames or relabels one asset attached to a GitHub release.") .account("github", GITHUB_CONTENTS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Repository-scoped release asset ID. */ assetId: z.number().int().positive(), /** Replacement display label. Use an empty string to remove the label. */ label: z.string().optional(), /** Replacement file name. */ name: z.string().min(1).optional(), }).refine( (input) => input.label !== undefined || input.name !== undefined, { error: "Provide a release asset name or label to update." }, ), ) .output(GITHUB_RELEASE_ASSET_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGitHubApi( account.secret, ).rest.repos.updateReleaseAsset({ asset_id: input.assetId, owner: input.owner, repo: input.repository, ...(input.label !== undefined && { label: input.label }), ...(input.name !== undefined && { name: input.name }), }) return data }) /** Deletes one asset attached to a GitHub release. */ export const deleteGitHubReleaseAsset = defineAction( "Delete GitHub release asset", ) .describe("Deletes one asset attached to a GitHub release.") .account("github", GITHUB_CONTENTS_WRITE_SCOPE) .input( GITHUB_REPOSITORY_INPUT_SCHEMA.extend({ /** Repository-scoped release asset ID. */ assetId: z.number().int().positive(), }), ) .output(GITHUB_EMPTY_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getGitHubApi(account.secret).rest.repos.deleteReleaseAsset({ asset_id: input.assetId, owner: input.owner, repo: input.repository, }) return {} })