Build TypeScript scripts and run them via actions/github-script.
This composite action bundles your external .ts files with esbuild into a single ESM module and then executes them through github-script, returning the result as a step output.
- ✅ Write CI logic in TypeScript (great editor DX).
- ✅ Bundle deps (e.g.,
axios,github-typescript-utilsfor GitHub Actions utilities) — no runtime installs needed after build. - ✅ Fast (esbuild) + optional bundle caching.
- ✅ Mirrors key
github-scriptinputs likegithub-token,result-encoding,retries, etc. - ✅ Lets you choose the Node target for bundling (e.g., 20/22).
- 🧰 No hidden setup: You manage
setup-nodeand dependency installs in your workflow (documented patterns below).
name: demoon: { workflow_dispatch: {} }jobs:
run-ts-script:
runs-on: ubuntu-lateststeps:
- uses: actions/checkout@v4# Install deps at repo root (or see "Isolate CI deps" below)
- uses: actions/setup-node@v4with:
node-version: 22cache: pnpm
- run: pnpm install
- name: Run TS via wrapperid: runuses: tkstang/github-typescript@v1with:
ts-file: .github/scripts/fetch-status.tsnode-version: '22'args: | { "url": "https://example.com/health" } - name: Show resultrun: echo '${{ steps.run.outputs.result }}'Your script must export a default async function:
export default async function run({ core, github, context, args }) { /* ... */ }
| Name | Required | Default | Description |
|---|---|---|---|
ts-file | ✅ | — | Path to your TypeScript entry file (relative to working-directory). |
args | "{}" | JSON string passed as args to your script's default export. Use multiline YAML format for complex objects. | |
working-directory | "." | Directory where bundling and imports resolve; bundle outputs go to ./.github-script-build. | |
node-version | "22" | Node target for bundling (affects esbuild --target=nodeXX). | |
esbuild-version | "0.24.0" | esbuild version used to bundle. |
| Name | Required | Default | Description |
|---|---|---|---|
github-token | ${{ github.token }} | Token used by Octokit. Provide a PAT/installation token if you need extra scopes. | |
debug | ${{ runner.debug == '1' }} | Whether to log GitHub client request details. Defaults to runner debug mode. | |
user-agent | "actions/github-script" | Optional user-agent string for GitHub API requests. | |
result-encoding | "json" | "json" or "string". Controls how the return value is encoded into the step output. | |
retries | "0" | The number of times to retry a request. | |
retry-exempt-status-codes | "400,401,403,404,422" | Comma‑separated HTTP status codes that will not be retried. | |
previews | "" | Comma‑separated GraphQL API preview names to enable. | |
base-url | "" | Optional GitHub REST API URL for GitHub Enterprise Server instances. |
| Name | Description |
|---|---|
result | The value returned by your script's default export, encoded according to result-encoding. |
Example in a step:
- name: Show resultrun: echo '${{ steps.run.outputs.result }}'Type signature you get:
typeContext={core: typeofimport("@actions/core");github: ReturnType<typeofimport("@actions/github").getOctokit>;context: typeofimport("@actions/github").context;args: unknown;// whatever you pass via `with.args`};exportdefaultasyncfunctionrun({ core, github, context, args }: Context){// ...return{ok: true};}Example:
// .github/scripts/fetch-status.tsimportaxiosfrom"axios";typeArgs={url: string};exportdefaultasyncfunctionrun({ core, args }: {core: any;args: Args}){if(!args?.url)thrownewError("args.url is required");constres=awaitaxios.get(args.url,{timeout: 5000});core.info(`GET ${args.url} -> ${res.status}`);return{status: res.status,ok: res.status>=200&&res.status<300};}repo-root/
package.json # axios etc.
node_modules/
.github/scripts/
fetch-status.ts
Workflow:
- uses: actions/setup-node@v4with:
node-version: 22cache: pnpm
- run: pnpm installrepo-root/
.github/scripts/
package.json
pnpm-lock.yaml
node_modules/
fetch-status.ts
Workflow:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4with: { version: 10 }
- uses: actions/setup-node@v4with:
node-version: 22cache: pnpmcache-dependency-path: .github/scripts/pnpm-lock.yaml
- run: pnpm installworking-directory: .github/scripts
- uses: tkstang/github-typescript@v1with:
working-directory: .github/scriptsts-file: fetch-status.tsnode-version: "22"- uses: pnpm/action-setup@v4with: { version: 10 }
- uses: actions/setup-node@v4with:
node-version: 22cache: pnpmcache-dependency-path: .github/scripts/pnpm-lock.yaml
- run: pnpm install --frozen-lockfileworking-directory: .github/scriptsThe wrapper sets
NODE_PATHto${{ inputs.working-directory }}/node_modulesso esbuild resolves your deps from that location when bundling.
This action can cache the compiled bundle to skip rebuilds when source and lockfiles haven't changed. It writes to ${working-directory}/.github-script-build/out.mjs.
Cache key factors:
- OS, Node target, esbuild version
hashFiles('**/*.ts', '**/*.tsx', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock')- the
ts-filepath
Pass static arguments:
- uses: tkstang/github-typescript@v1with:
ts-file: .github/scripts/my-task.tsargs: | { "environment": "production", "retries": 3, "endpoints": ["api1", "api2"] }Pass dynamic arguments from step outputs:
- name: Get deployment infoid: deployrun: | echo "version=1.2.3" >> $GITHUB_OUTPUT echo "region=us-east-1" >> $GITHUB_OUTPUT echo "config={\"database\":\"prod\",\"replicas\":3}" >> $GITHUB_OUTPUT- uses: tkstang/github-typescript@v1with:
ts-file: .github/scripts/deploy.tsargs: '${{ toJson(steps.deploy.outputs) }}'Return JSON result:
- uses: tkstang/github-typescript@v1id: mystepwith:
ts-file: .github/scripts/my-task.tsresult-encoding: json
- run: echo "${{ steps.mystep.outputs.result }}"Return string result:
- uses: tkstang/github-typescript@v1id: mystepwith:
ts-file: .github/scripts/my-task.tsresult-encoding: stringControl retries & token:
- uses: tkstang/github-typescript@v1with:
ts-file: .github/scripts/triage.tsgithub-token: ${{ secrets.GH_PAT_WITH_SCOPES }}retries: 3retry-exempt-status-codes: "400,401"- Pin upstream actions (e.g.,
actions/checkout,actions/setup-node) by commit SHA in sensitive workflows. - Use least‑privilege
permissions:blocks. Example:permissions: contents: readpull-requests: write # only if your script needs itactions: write # only if touching Actions API
- Validate any untrusted input used by your scripts before shelling out.
- Prefer returning small results; for large payloads, write files and upload artifacts.
- Tag majors (
v1) and keep them backwards compatible. - For maximum supply‑chain control, consumers can pin to a commit SHA.
- Keep
esbuild-versioncurrent for best ESM/TS support.
- Cannot find module 'axios' → ensure you installed deps where
working-directorycan see them and that the wrapper's build step setsNODE_PATHaccordingly. TypeError: run is not a function→ your script must default export a function (export default async function run(...) {}).- YAML parsing errors with
args→ for static objects, use multiline YAML format:For dynamic values from step outputs or variables, use quotedargs: | { "key": "value", "array": ["item1", "item2"] }
toJson():# Pass all step outputs as an objectargs: '${{ toJson(steps.previous.outputs) }}'# Pass workflow variablesargs: '${{ toJson(vars) }}'# If you need to parse a JSON string first, use fromJson() (but not with toJson())# Example: steps.data.outputs.config = '{"key": "value"}' - run: echo "Key is ${{ fromJson(steps.data.outputs.config).key }}"
- Output too large → use artifacts instead of step outputs, or switch
result-encodingtostringif you only need a short message. - Cache misses → narrow
hashFiles(...)to your scripts subdir and lockfile; keep Node/esbuild versions consistent.
Q: Do I need node_modules at runtime?
A: No. After bundling, github-script imports a single out.mjs bundle.
Q: Can I share common helpers across repos?
A: Yes—publish a small ESM utils package (e.g., github-typescript-utils for GitHub Actions workflow utilities) and install it in the repo/sub‑package. The wrapper will bundle it.
Q: Does this replace actions/github-script?
A: No. It wraps it, so you keep Octokit/context ergonomics and its features (retries, result encoding, etc.).
A companion TypeScript utilities package for GitHub Actions workflows. Provides REST API helpers, context utilities, and common workflow functions.
// Example: .github/scripts/pr-manager.tsimport{getRepoInfo,createStickyComment}from'github-typescript-utils';exportdefaultasyncfunctionrun({ core, github, context, args }){constctx={ core, github, context };constrepo=getRepoInfo(ctx);awaitcreateStickyComment({
ctx, repo,issueNumber: context.issue.number,identifier: 'welcome',body: `Welcome! This PR is for ${repo.owner}/${repo.repo}`});}See the utils README for full usage and installation.
MIT