Important
This project is a demo GitHub Action built to educate developers on building TypeScript-based GitHub Actions with best practices. For in-depth look at this action, check the blog post series over at Polar Squad website.
A custom GitHub Action built with Bun and TypeScript transforming strings using the ROT-13 cipher. This action demonstrates best practices for building GitHub Actions with modern JavaScript tooling, clean architecture, property-based testing, and comprehensive mutation testing.
| name | description | required | default |
|---|---|---|---|
string |
String to transform |
true |
"" |
| name | description |
|---|---|
result |
Result of the transformation |
This action is a node24 action.
- uses: nikoheikkila/rot-13-action@v1
with:
string:
# String to transform
#
# Required: true
# Default: ""- 🚀 Bun-powered toolchain: Fast builds and testing with Bun runtime
- 📐 Clean architecture: Dependency injection pattern for testability
- ✅ Comprehensive test coverage: Unit tests with test doubles (no mocks)
- 🧪 Property-based testing: Using fast-check to verify ROT-13 properties
- 🦠 Mutation testing: Stryker Mutator ensures test quality and effectiveness
- 🛠️ Task automation: Managed with Task
- 🪝 Pre-commit hooks: Automatic testing and building via Husky
- 📦 Single bundle distribution: Optimized for GitHub Actions runtime
In case you didn't know, ROT-13 is a simple letter substitution cipher that rotates letters 13 positions in the alphabet:
- A → N, B → O, M → Z, N → A, Z → M
- Lowercase letters are transformed similarly: a → n, z → m
- Non-alphabetic characters (numbers, punctuation, spaces) remain unchanged
- Idempotent: Applying ROT-13 twice returns the original text
Example: "Hello, World!" → "Uryyb, Jbeyq!"
The action follows a clean architecture pattern with clear separation of concerns:
bin/index.ts: Invokes the action with production dependenciessrc/action.ts: Main action logic inRot13GitHubActionclasssrc/input.ts: Input parsing and validation logicsrc/rot13.ts: ROT-13 transformer implementationsrc/types.ts: Type definitions forCoreandGitHubinterfacestests/: Test doubles (FakeCore) and comprehensive test suites
This design uses dependency injection to make the action fully testable without mocking the GitHub Actions toolkit.
Install dependencies using Task:
task installRun all the tests once:
task testRun unit tests in watch mode:
task test:watchBuild the action for distribution:
task buildThis bundles bin/index.ts into dist/index.js with:
- Minification enabled
- ESM format targeting Node.js 20
Husky automatically runs tests and builds before each commit. This ensures dist/ is always up to date with your
source code.
- Source code is written in TypeScript under
src/with business logic andbin/for the entry point - Bun bundles
bin/index.tsand outputs todist/index.js - Pre-commit hook ensures tests pass and the bundle is up to date
- GitHub Actions runs the
dist/index.jsbundle using Node.js 24 runtime
The bundled JavaScript file is committed to version control because GitHub Actions cannot run build steps when executing custom actions.
This project employs multiple testing approaches to ensure code quality:
Tests use Bun's built-in test runner and follow these patterns:
- Test doubles (
FakeCore) implement the same interfaces as@actions/corepackage. - Dependency injection allows testing without real GitHub Actions context
- Event tracking in
FakeCorecaptures all logged messages and outputs for verification
The ROT-13 implementation is verified using fast-check to test mathematical properties:
- Idempotency: The text transformed twice equals the input
- Length preservation: Output length equals input length
- Case preservation: Uppercase letters remain uppercase, lowercase remain lowercase
- Non-alphabetic preservation: Numbers, punctuation, and special characters are unchanged
These properties are tested against thousands of randomly generated inputs to ensure correctness.
Stryker Mutator performs mutation testing to verify test effectiveness:
- Introduces small changes (mutants) to the source code
- Ensures tests catch these mutants (kills them)
- High mutation score indicates strong test coverage and quality
Run mutation tests with:
task test:mutation