TypeScript utilities for GitHub Actions workflows with github-typescript.
This package provides a comprehensive set of TypeScript utilities for GitHub Actions workflows, including REST API interactions, Octokit helpers, context utilities, and common workflow functions. Designed specifically for use with the github-typescript composite action, these utilities simplify working with GitHub's API, Actions context, and core functionality.
| Category | Functions | Description |
|---|---|---|
| π¬ Comments | createStickyComment, findCommentByIdentifier, searchComments, deleteComment, deleteStickyComment |
Create, find, search, and manage issue/PR comments |
| π Pull Requests | findPullRequestsByLabels, getPullRequest, addLabelsToPullRequest, removeLabelsFromPullRequest, pullRequestHasLabels, getPullRequestFiles |
Find, manage, and interact with pull requests |
| π Advanced PR Search | findPRsWithLabels, searchPullRequests, findOpenPRsWithLabel, checkLabelConflicts |
Advanced pull request search and label conflict detection |
| πΏ Branch Management | checkBranchExists, listAllBranches, getBranchProtection, getDefaultBranch |
Branch existence, listing, and protection management |
| π Deployments | listDeployments, getDeploymentStatuses, setDeploymentStatus, deleteDeployment, createDeployment |
Deployment lifecycle management |
| π§ Context & Utils | getRepoInfo, getCurrentPullRequestNumber, getCurrentIssueNumber, isPullRequestContext, isIssueContext, getCurrentSHA, getCurrentBranch, getRepositoryUrl, getIssueUrl, getPullRequestUrl |
GitHub Actions context extraction and URL helpers |
| π Text & Formatting | escapeMarkdown, codeBlock, createMarkdownTable, truncateText, formatDate, parseGitHubDate, delay |
Text formatting, markdown utilities, and date handling |
| π€ String Utilities | snakeToCamel, camelToSnake, kebabToCamel, camelToKebab, capitalize, toTitleCase |
String case conversion and text transformation |
| βοΈ Input Processing | sanitizeInput, sanitizeInputs, getBranch |
Input sanitization and branch extraction from various GitHub events |
- β Type Safety - Full TypeScript support with comprehensive type definitions
- β Universal Compatibility - Works with all GitHub event types and contexts
- β Comprehensive Testing - 110+ tests with 94%+ code coverage
- β GitHub API Optimized - Efficient API usage with proper error handling and pagination support
- β Developer Friendly - Intuitive APIs with TypeScript intellisense and JSDoc documentation
# With pnpm (recommended) pnpm add github-typescript-utils # With npm npm install github-typescript-utils # With yarn yarn add github-typescript-utils
Peer Dependencies:
@actions/core(^1.10.0)@actions/github(^6.0.0)
For esbuild to resolve github-typescript-utils, you need the package available in node_modules. Choose one approach:
Add to your repository's root package.json:
{
"dependencies": {
"github-typescript-utils": "^0.2.0"
}
}Workflow setup:
- uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install - uses: tkstang/github-typescript@v1 with: ts-file: .github/scripts/manage-pr.ts
Create .github/scripts/package.json:
{
"name": "ci-scripts",
"private": true,
"type": "module",
"dependencies": {
"github-typescript-utils": "^0.2.0"
}
}Workflow setup:
- uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm cache-dependency-path: .github/scripts/pnpm-lock.yaml - run: pnpm install working-directory: .github/scripts - uses: tkstang/github-typescript@v1 with: working-directory: .github/scripts ts-file: manage-pr.ts
Create a TypeScript script that imports the utilities:
// .github/scripts/manage-pr.ts import { createStickyComment, findPullRequestsByLabels, getRepoInfo, getCurrentPullRequestNumber, type GitHubContext, } from "github-typescript-utils"; type Args = { message: string; labels: string[]; }; export default async function run({ core, github, context, args, }: GitHubContext & { args: Args }) { const repo = getRepoInfo({ core, github, context }); const prNumber = getCurrentPullRequestNumber({ core, github, context }); if (!prNumber) { core.setFailed("This action must run on a pull request"); return; } // Create a sticky comment await createStickyComment({ ctx: { core, github, context }, repo, issueNumber: prNumber, options: { identifier: "status-update", body: `## PR Status\n\n${args.message}`, }, }); // Find PRs with specific labels const prs = await findPullRequestsByLabels({ ctx: { core, github, context }, repo, options: { labels: args.labels }, }); core.info(`Found ${prs.length} PRs with labels: ${args.labels.join(", ")}`); return { success: true, prCount: prs.length }; }
Use in your workflow:
- name: Manage PR uses: tkstang/github-typescript@v1 with: ts-file: .github/scripts/manage-pr.ts args: ${{ toJson({ message: 'Build completed!', labels: ['ready-for-review'] }) }}
Creates or updates a "sticky" comment that can be updated in subsequent runs rather than creating duplicates.
await createStickyComment({ ctx: { core, github, context }, repo: { owner: "tkstang", repo: "my-repo" }, issueNumber: 123, options: { identifier: "build-status", body: "β Build completed successfully!", updateIfExists: true, // default: true }, });
Search for comments based on various criteria:
const comments = await searchComments({ ctx: { core, github, context }, repo, issueNumber: 123, options: { bodyContains: "error", author: "dependabot[bot]", createdAfter: new Date("2024-01-01"), limit: 10, }, });
Remove comments by ID or sticky identifier:
// Delete by ID await deleteComment({ ctx: { core, github, context }, repo, commentId: 456789, }); // Delete sticky comment await deleteStickyComment({ ctx: { core, github, context }, repo, issueNumber: 123, identifier: "build-status", });
Find pull requests that have all specified labels:
const prs = await findPullRequestsByLabels({ ctx: { core, github, context }, repo, options: { labels: ["bug", "ready-for-review"], state: "open", // 'open' | 'closed' | 'all' limit: 20, }, });
Manage PR labels:
await addLabelsToPullRequest({ ctx: { core, github, context }, repo, pullNumber: 123, labels: ["approved", "ready-to-merge"], }); await removeLabelsFromPullRequest({ ctx: { core, github, context }, repo, pullNumber: 123, labels: ["work-in-progress"], });
Get the list of files changed in a PR:
const files = await getPullRequestFiles({ ctx: { core, github, context }, repo, pullNumber: 123, }); console.log( files.map((f) => `${f.filename} (+${f.additions}/-${f.deletions})`) );
// Get repository info from context const repo = getRepoInfo({ core, github, context }); // Returns: { owner: 'tkstang', repo: 'my-repo' } // Get current PR/issue number const prNumber = getCurrentPullRequestNumber({ core, github, context }); const issueNumber = getCurrentIssueNumber({ core, github, context }); // Check context type const isPR = isPullRequestContext({ core, github, context }); const isIssue = isIssueContext({ core, github, context }); // Get commit/branch info const sha = getCurrentSHA({ core, github, context }); const branch = getCurrentBranch({ core, github, context });
const repoUrl = getRepositoryUrl({ core, github, context }); const prUrl = getPullRequestUrl({ core, github, context }, 123); const issueUrl = getIssueUrl({ core, github, context }, 456);
import { escapeMarkdown, codeBlock, createMarkdownTable, truncateText, } from "github-typescript-utils"; // Escape special markdown characters const safe = escapeMarkdown("Text with *special* characters"); // Create code blocks const code = codeBlock('console.log("hello");', "javascript"); // Create tables const table = createMarkdownTable( ["File", "Status", "Changes"], [ ["src/index.ts", "β ", "+15/-3"], ["README.md", "π", "+2/-0"], ] ); // Truncate long text const short = truncateText("Very long text...", 50);
import { snakeToCamel, camelToSnake, kebabToCamel, camelToKebab, capitalize, toTitleCase, } from "github-typescript-utils"; // Convert between naming conventions const camelCase = snakeToCamel("hello_world"); // "helloWorld" const snakeCase = camelToSnake("helloWorld"); // "hello_world" const kebabCase = camelToKebab("helloWorld"); // "hello-world" const camelFromKebab = kebabToCamel("hello-world"); // "helloWorld" // Text transformation const capitalized = capitalize("hello"); // "Hello" const titleCase = toTitleCase("hello world"); // "Hello World"
import { checkBranchExists, listAllBranches, getBranchProtection, getDefaultBranch, } from "github-typescript-utils"; // Check if branch exists const exists = await checkBranchExists({ ctx: { core, github, context }, repo, branch: "feature-branch", }); // List all branches const branches = await listAllBranches({ ctx: { core, github, context }, repo, limit: 50, }); // Get branch protection rules const protection = await getBranchProtection({ ctx: { core, github, context }, repo, branch: "main", }); // Get default branch const defaultBranch = await getDefaultBranch({ ctx: { core, github, context }, repo, });
import { listDeployments, createDeployment, setDeploymentStatus, getDeploymentStatuses, deleteDeployment, } from "github-typescript-utils"; // Create a deployment const deployment = await createDeployment({ ctx: { core, github, context }, repo, ref: "main", environment: "production", description: "Deploy v1.0.0", }); // Set deployment status await setDeploymentStatus({ ctx: { core, github, context }, repo, deploymentId: deployment.id, state: "success", description: "Deployment completed successfully", }); // List deployments const deployments = await listDeployments({ ctx: { core, github, context }, repo, environment: "production", });
import { findPRsWithLabels, searchPullRequests, checkLabelConflicts, findOpenPRsWithLabel, } from "github-typescript-utils"; // Find PRs with multiple labels const prs = await findPRsWithLabels({ ctx: { core, github, context }, repo, labels: ["bug", "urgent"], excludePRs: [123], // Exclude specific PR numbers }); // Advanced PR search const searchResults = await searchPullRequests({ ctx: { core, github, context }, repo, options: { labels: ["feature"], author: "dependabot[bot]", state: "open", }, }); // Check for label conflicts const conflict = await checkLabelConflicts({ ctx: { core, github, context }, repo, prNumber: 123, label: "sync-branch: main", }); if (conflict.hasConflict) { core.warning(`Label conflict with PR #${conflict.conflictingPR?.number}`); }
import { sanitizeInput, sanitizeInputs, getBranch, } from "github-typescript-utils"; // Remove quotes from workflow inputs const cleanInput = sanitizeInput('"quoted-value"'); // "quoted-value" // Sanitize all string properties in an object const cleanInputs = sanitizeInputs({ name: '"John"', age: 30, title: '"Developer"', }); // { name: "John", age: 30, title: "Developer" } // Extract branch from any GitHub event const branch = getBranch({ core, github, context }); // Works with: pull_request, push, workflow_run, etc.
The package exports comprehensive TypeScript types:
import type { // Core types GitHubContext, RepoInfo, PullRequest, IssueComment, // Comment types StickyCommentOptions, CommentSearchOptions, // Pull request types PullRequestSearchOptions, PullRequestFile, AdvancedPRSearchOptions, // Deployment types Deployment, DeploymentStatus, } from "github-typescript-utils";
// .github/scripts/update-build-status.ts import { createStickyComment, getRepoInfo, getCurrentPullRequestNumber, } from "github-typescript-utils"; export default async function run({ core, github, context, args }) { const repo = getRepoInfo({ core, github, context }); const prNumber = getCurrentPullRequestNumber({ core, github, context }); if (!prNumber) return { skipped: true }; const status = args.success ? "β Passed" : "β Failed"; const details = args.details || ""; await createStickyComment({ ctx: { core, github, context }, repo, issueNumber: prNumber, options: { identifier: "ci-status", body: `## π CI Status\n\n${status}\n\n${details}`, }, }); return { updated: true }; }
// .github/scripts/triage-prs.ts import { findPullRequestsByLabels, addLabelsToPullRequest, getRepoInfo, } from "github-typescript-utils"; export default async function run({ core, github, context }) { const repo = getRepoInfo({ core, github, context }); // Find stale PRs const stalePRs = await findPullRequestsByLabels({ ctx: { core, github, context }, repo, options: { labels: ["needs-review"], state: "open", sort: "updated", direction: "asc", limit: 10, }, }); // Label old PRs as stale for (const pr of stalePRs) { const daysSinceUpdate = (Date.now() - new Date(pr.updated_at).getTime()) / (1000 * 60 * 60 * 24); if (daysSinceUpdate > 7) { await addLabelsToPullRequest({ ctx: { core, github, context }, repo, pullNumber: pr.number, labels: ["stale"], }); core.info( `Labeled PR #${pr.number} as stale (${Math.round( daysSinceUpdate )} days old)` ); } } return { processed: stalePRs.length }; }
- Clone the repository
- Install dependencies:
pnpm install - Make changes to
src/files - Build:
pnpm run build - Test your changes
- Submit a pull request
MIT - see LICENSE file for details.