actions/typescript-action: a template for building GitHub Actions in TypeScript
Create a TypeScript Action with tests, linting, workflow, publishing, and versioning
At a glance
- What is it?
- The actions/typescript-action template ships the build, test, lint and publishing scaffolding for a compiled TypeScript GitHub Action, so you write src/main.ts and let rollup produce the dist/ bundle the runner actually executes.
- Who is it for?
- Adopt this template if you are writing a JavaScript or TypeScript GitHub Action and want the compiled-bundle workflow, a test harness and linting already wired together, and you can accept that every source change must be followed by npm run bundle before the action behaves correctly. Do not adopt it if you only need a few lines of shell, where a composite action in YAML is simpler and needs no build step, or if you cannot commit build output to your repository.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 72 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What actions/typescript-action solves, and who it is for
A GitHub Action written in JavaScript or TypeScript is not run from source. The runner downloads the repository and executes a single bundled entry point, which for this template is dist/index.js, declared through the exports field in package.json. That means the author has to bundle TypeScript and its dependencies into one file before publishing. Doing that by hand is where most first-time action authors lose an afternoon: the build tool, the test runner, the linter, the metadata file and the version tags all have to agree before a workflow can call the action at all. This repository is a GitHub template, not a library you install. Its value is the wiring already in place: rollup with the TypeScript plugin and commonjs and node-resolve plugins, Jest with the experimental VM modules flag, ESLint, Prettier, markdown and YAML lint configuration, CodeQL analysis, a check-dist workflow, and a coverage badge. The audience is narrow and clear. It is for someone who has decided the action logic is worth writing in TypeScript with tests, and who wants the repository skeleton rather than a tutorial. The README points people who want something smaller at the hello-world JavaScript action repository, and that pointer is honest about the trade-off: this template carries more moving parts than a first action needs.
How the template is put together: src/, dist/ and action.yml
The data flow has three stages and one hard boundary. You write code in src/, with src/main.ts as the entry point that the local-action tool and the workflow both target. The action is invoked inside an async function so that toolkit calls, which the README describes as mostly asynchronous, can be awaited; errors are routed to core.setFailed(error.message) inside a catch block. From src/, rollup produces dist/, and dist/ is what the runner executes when a workflow references the action. The boundary is this: dist/ is generated, but it is committed, because the runner does not run npm install for a JavaScript action. The repository layout reflects that, with dist/ sitting at the top level next to src/ and a dedicated check-dist workflow whose purpose is to catch a dist/ that has drifted from its sources. The third piece is action.yml, the metadata file that declares the action's name, description, inputs and outputs. The README states plainly that when you copy the repository you must update action.yml, and that src/ is the directory you replace with your own code. Everything else (the lint configs, the test directory, the fixtures directory, the devcontainer) is supporting structure around those three.
Installing the template and running the sample action
There is no package to install globally. You create a repository from the template on GitHub, clone it, and work locally. The README asks for a modern Node.js, noting that 20.x or later should work, and the repository carries a .node-version file that version managers such as nodenv or fnm can read, and that actions/setup-node consumes in workflows. Note that package.json declares an engines field of node >=24.0.0, which is stricter than the README's prose; treat the engines field as the binding constraint for local tooling. After cloning, install dependencies and produce the bundle:
npm install
npm run bundleThe bundle script runs Prettier in write mode and then the package script, which deletes dist/ with rimraf and rebuilds it with rollup using rollup.config.ts. Run the tests next; the README shows the expected output as a Jest run over index.test.js with cases such as throws invalid number and wait 500 ms.
npm testTo try the action without pushing anything, the template wires in @github/local-action, which stubs the GitHub Actions toolkit. The npm script passes the action metadata path, the entry point and a dotenv file:
npm run local-actionThat script is equivalent to npx @github/local-action . src/main.ts .env. The repository ships .env.example, which you copy to .env; inputs are named INPUT_<name> in upper case with hyphens preserved, so the sample input appears as INPUT_MILLISECONDS=2400, and ACTIONS_STEP_DEBUG=true turns on step debug logging. Finally, to exercise the action inside a real workflow, the README shows referencing it from the same repository with uses: ./ and passing milliseconds: 1000, then reading the output with steps.test-action.outputs.time. Before any of that, remove or update the CODEOWNERS file, which the README flags as important.
The dist/ commit requirement is the template's sharpest edge
The most common failure with this template is an action that works locally and does nothing useful in a workflow, because the author edited src/ and pushed without rebuilding. The README is explicit that the all script matters: it runs format:write, lint, test, coverage and package in sequence, and the note attached to it warns that skipping the packaging step means the action will not work correctly when used in a workflow. That is not a stylistic preference. The runner fetches the repository and executes the committed bundle, so an unbuilt change is invisible at runtime. The check-dist workflow exists to make the drift visible in CI, but it can only report the problem, not prevent a release tag from pointing at a stale bundle. Two smaller edges are worth knowing. First, the engines field requires Node 24 while the README says 20.x or later should work, so a contributor on Node 20 may hit a mismatch that the prose does not prepare them for. Second, the template's own sample logic is a delay driven by the milliseconds input, which is fine as a demonstration but tells you nothing about how your action behaves against the GitHub API, rate limits or large event payloads. Local stubbing through @github/local-action covers inputs and environment variables; it does not reproduce runner behaviour.
Composite actions and the hello-world JavaScript action as alternatives
The real alternative for many small tasks is a composite action: a YAML file with a runs.using value of composite that lists shell steps directly. There is no build step, no dist/ directory to commit, no Node runtime to pin, and no bundler configuration to maintain. The difference in approach is where the logic lives. A composite action composes existing commands and actions in YAML; this template compiles TypeScript into a single JavaScript bundle that the runner executes with Node. If your action is three curl calls and a conditional, the composite route is less machinery for the same result. If you need typed data structures, unit tests over parsing logic, or dependencies from npm, the compiled route is the one that scales, and that is the gap this template fills. The README also names the hello-world JavaScript action repository as a simpler introduction for newcomers, which is a different trade-off again: less scaffolding to learn, but also no bundling, testing or linting setup to inherit. Choosing between them is mostly a question of whether the action's logic deserves tests. If it does, starting from this template costs you a build step and a dist/ commit on every change. If it does not, the build step is pure overhead.
Maintenance, licensing and the cost of keeping the toolchain current
The repository is not archived, and the last push was on 2026-07-20. The template pins a broad devDependency set: ESLint with the compat and eslintrc bridges, Jest 30 with the globals package, the rollup plugin family, TypeScript types for Node, and @github/local-action. Because these are devDependencies of your copied repository, not of a package you consume, upgrades are your responsibility once you create your repository from the template. Nothing in the template updates itself. The practical cost is concentrated in three places: the rollup configuration when a plugin's major version changes, the Jest invocation, which depends on NODE_OPTIONS=--experimental-vm-modules because the package is an ES module, and the engines field when GitHub's runner images move to a new Node major. The licence is MIT, which the LICENSE file and the package.json license field both state. MIT is permissive and places few conditions on redistribution, but the template also carries a .licensed.yml file and a .licenses/ directory, which suggests the project tracks dependency licences for its own build. If you copy the repository, that tracking does not automatically cover the dependencies you add. This is a description of what the files say, not legal advice; review the licences of anything you add before you publish.
Editorial conclusion
Adopt this template if you are writing a JavaScript or TypeScript GitHub Action and want the compiled-bundle workflow, a test harness and linting already wired together, and you can accept that every source change must be followed by npm run bundle before the action behaves correctly. Do not adopt it if you only need a few lines of shell, where a composite action in YAML is simpler and needs no build step, or if you cannot commit build output to your repository. Before you rely on it, verify that your Node version satisfies the package.json engines field, and confirm that removing or updating CODEOWNERS and replacing the action.yml name, description, inputs and outputs are the first two edits you make.
Frequently asked questions
How do I write a GitHub Action with actions/typescript-action?
Create a repository from the template, clone it, and replace the contents of src/ with your own code while keeping src/main.ts as the entry point. Update action.yml with your action's name, description, inputs and outputs, then run npm run all, which formats, lints, tests, generates coverage and rebuilds dist/. Reference the action from a workflow with uses: ./ to test it in the same repository.
What Node.js version does the actions/typescript-action template need?
The README says a reasonably modern Node.js, noting that 20.x or later should work, and the repository includes a .node-version file that version managers and actions/setup-node can read. The package.json engines field is stricter, declaring node >=24.0.0, so that is the constraint to satisfy for local tooling.
Why does my actions/typescript-action change not take effect in a workflow?
The runner executes the committed bundle in dist/, not the TypeScript in src/. If you edited src/ and did not rebuild, the workflow runs the old code. Run npm run bundle, or npm run all, and commit the regenerated dist/ directory; the repository also has a check-dist workflow that flags a dist/ out of sync with its sources.
Can I test an actions/typescript-action action locally without pushing?
Yes. The template depends on @github/local-action, which stubs the GitHub Actions Toolkit, and exposes it as the local-action npm script, equivalent to npx @github/local-action . src/main.ts .env. Copy .env.example to .env and set inputs in INPUT_<name> form, such as INPUT_MILLISECONDS=2400; the README also documents a Visual Studio Code debugger configuration in .vscode/launch.json.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/actions-typescript-action)