github-actions-workflow-ts
v0.3.2
Published
Generate GitHub Actions workflow files using TypeScript (compiles to YAML)
Downloads
4,103
Readme
github-actions-workflow-ts
Stop writing workflows in YAML and use TypeScript instead!
Table of Contents
- github-actions-workflow-ts
Installation
npm install --save-dev github-actions-workflow-ts
Overview
Introducing github-actions-workflow-ts
: A seamless integration allowing developers to author GitHub Actions workflows with the power and flexibility of TypeScript.
Key Benefits:
- Type Safety: Elevate the confidence in your workflows with the robust type-checking capabilities of TypeScript.
- Modularity: Efficiently package and reuse common jobs and steps across various workflows, promoting the DRY (Don't Repeat Yourself) principle.
- Control Flow: Harness the inherent control flow mechanisms, like conditionals, available in imperative languages. This empowers developers to craft intricate workflows beyond the constraints of YAML.
Getting Started:
To embark on this efficient journey, create a new *.wac.ts
file, for instance, deploy.wac.ts
, in your project directory. Then, dive into authoring your enhanced GitHub Actions workflows!
Examples
Try it out on Replit
Want to quickly see it in action? Explore these Replit examples (create a free account to fork and modify my examples):
More Examples
Check the examples folder and the workflows folder for more advanced examples.
Below is a simple example:
// example.wac.ts
import { Workflow, NormalJob, Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
// IMPORTANT - the instance of Workflow MUST be exported with `export`
export const exampleWorkflow = new Workflow('example-filename', {
name: 'Example',
on: {
workflow_dispatch: {}
}
})
// add the defined step to the defined job
testJob.addStep(checkoutStep)
// add the defined job to the defined workflow
exampleWorkflow.addJob(testJob)
Generating Workflow YAML
Using the CLI
When you have written your *.wac.ts
file, you use the github-actions-workflow-ts
CLI to generate the yaml files.
Don't forget to export the workflows that you want to generate in your *.wac.ts
files i.e.
// exporting `exampleWorkflow` will generate example-filename.yml
export const exampleWorkflow = new Workflow('example-filename', { /***/ })
Then, from project root, run:
npx generate-workflow-files build
# OR
npx gwf build
Integration with Husky (recommended)
For seamless automation and to eliminate the possibility of overlooking updates in *.wac.ts
files, integrating with a pre-commit tool is recommended. We recommend husky. With Husky, each commit triggers the npx github-actions-workflow-ts build
command, ensuring that your GitHub Actions YAML files consistently reflect the latest modifications.
- Install Husky:
npm install --save-dev husky npx husky-init
- In
package.json
, add the following script:"scripts": { "build:workflows": "npx gwf build && git add .github/workflows/*.yml", }
- Install the
pre-commit
command to Husky and add our npm command to build the*.wac.ts
filesnpx husky add .husky/pre-commit "npm run build:workflows"
- Now every time you make a change to
*.wac.ts
, Husky will run thenpx gwf build
command and add the generated.github/workflows/*.yml
to your commit
Config file
If you want to change how github-actions-workflow-ts generates the yaml files, you can create a wac.config.json
file in your project root. See the example config file
| Property | Description | Type | Default Value |
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| refs | If true, convert duplicate objects into references in YAML | Boolean
| false |
| headerText | Replace the header text in generated YAML files with your own text. If you want the source filename and path in the text, use <source-file-path>
inthe text and it will be replaced with the path to the source-file. | Array<string>
| # ----DO-NOT-MODIFY-THIS-FILE----# This file was automatically generated by github-actions-workflow-ts. # Instead, modify <source-file-path>
# ----DO-NOT-MODIFY-THIS-FILE---- |
| dumpOptions | Options for the dump function of js-yaml. See all the options here | Record <string, any> | Uses the default options |
Workflow Classes
new Step()
The building block of every NormalJob
. Contains instructions on what to run in your Github Actions Runner in each job.
import { Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
.addEnvs()
This adds environment variables to a step.
import { Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
}).addEnvs({
SOME_KEY: 'some-value',
SOME_OTHER_KEY: 'some-other-value'
})
new NormalJob()
The most typical job that contains steps.
.addEnvs()
This adds environment variables to a job.
import { NormalJob } from 'github-actions-workflow-ts'
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
}).addEnvs({
SOME_KEY: 'some-value',
SOME_OTHER_KEY: 'some-other-value'
})
.addStep()
This adds a single step to a normal Job
import { Workflow, NormalJob, Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
testJob.addStep(checkoutStep)
.addSteps()
This adds multiple steps to a normal Job
import { Workflow, NormalJob, Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
const installNodeStep = new Step({
name: 'Install Node',
uses: 'actions/setup-node@v3',
with: {
'node-version': 18
}
})
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
testJob.addSteps([
checkoutStep,
installNodeStep
])
.needs()
This adds any jobs that the current job depends on to the current job's needs
property
import { Workflow, NormalJob, Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
const buildJob = new NormalJob('Build', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
testJob.addStep(checkoutStep)
buildJob
.needs([testJob])
.addStep(checkoutStep)
export const exampleWorkflow = new Workflow('example-filename', {
name: 'Example',
on: {
workflow_dispatch: {}
}
})
exampleWorkflow.addJobs([
testJob,
buildJob
])
new ReusableWorkflowCallJob()
A job that allows you to call another workflow and use it in the same run.
import { Workflow, ReusableWorkflowCallJob } from 'github-actions-workflow-ts'
const releaseJob = new ReusableWorkflowCallJob('ReleaseJob', {
uses: 'your-org/your-repo/.github/workflows/reusable-workflow.yml@main',
secrets: 'inherit',
})
export const exampleWorkflow = new Workflow('example-filename', {
name: 'Example',
on: {
workflow_dispatch: {}
}
}).addJob(releaseJob)
.needs()
Same as NormalJob.needs()
new Workflow()
.addEnvs()
This adds environment variables to a workflow.
import { Workflow } from 'github-actions-workflow-ts'
export const exampleWorkflow = new Workflow('example-filename', {
name: 'Example',
on: {
workflow_dispatch: {}
}
}).addEnvs({
SOME_KEY: 'some-value',
SOME_OTHER_KEY: 'some-other-value'
})
.addJob()
This adds a single job to a Workflow
import { Workflow, NormalJob, Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
testJob.addStep([checkoutStep])
export const exampleWorkflow = new Workflow('example-filename', {
name: 'Example',
on: {
workflow_dispatch: {}
}
})
exampleWorkflow.addJob(testJob)
.addJobs()
This adds multiple jobs to a Workflow
import { Workflow, NormalJob, Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
const buildJob = new NormalJob('Build', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
testJob.addStep(checkoutStep)
buildJob.addStep(checkoutStep)
export const exampleWorkflow = new Workflow('example-filename', {
name: 'Example',
on: {
workflow_dispatch: {}
}
})
exampleWorkflow.addJobs([
testJob,
buildJob
])
Workflow Types
You can also choose not to use the workflow helpers and just use plain old JSON. You get type safety by importing the types. The only exception is the Workflow
class. You must export an instance of this class in order to generate your workflow files.
GeneratedWorkflowTypes
These are types generated right out of the Github Actions Workflow JSON Schema
ExtendedWorkflowTypes
These are types that I extended myself because they weren't autogenerated from the JSON Schema.
import {
Workflow,
NormalJob,
Step,
expressions as ex,
ExtendedWorkflowTypes as EWT, // contains the Step and Steps types
GeneratedWorkflowTypes as GWT, // contains all the other types e.g. NormalJob, ReusableWorkflowCallJob etc
} from '../src'
const nodeSetupStep: EWT.Step = {
name: 'Setup Node',
uses: 'actions/setup-node@v3',
with: {
'node-version': '18.x',
},
}
const firstNormalJob: GWT.NormalJob = {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 5,
steps: [
nodeSetupStep,
{
name: 'Echo',
run: 'echo "Hello, World!"',
},
],
}
export const simpleWorkflowOne = new Workflow('simple-1', {
name: 'ExampleSimpleWorkflow',
on: {
workflow_dispatch: {},
},
jobs: {
firstJob: firstNormalJob,
},
})
Helpers
multilineString()
This is a useful function that aids in writing multiline yaml like this:
name: Run something
run: |-
command exec line 1
command exec line 2
Example 1
import { multilineString } from 'github-actions-workflow-ts'
// multilineString(...strings) joins all strings with a newline
// character '\n' which is interpreted as separate lines in YAML
console.log(multilineString('This is sentence 1', 'This is sentence 2'))
// 'This is sentence 1\nThis is sentence 2'
// it also has the ability to escape special characters
console.log(
multilineString(
`content="\${content//$'\n'/'%0A'}"`,
`content="\${content//$'\r'/'%0D'}"`
)
)
// `content="${content//$'\n'/'%0A'}"`
// `content="${content//$'\r'/'%0D'}"``
Example 2 - handling multiline string indentation If you want to do something like this
- name: Check for build directory
run: |-
#!/bin/bash
ls /tmp
if [ ! -d "/tmp/build" ]; then
mv /tmp/build .
ls
fi
then you just add the same indentation in the string:
// If you want indentation then you can do this:
new Step({
name: 'Check for build directory',
run: multilineString(
`#!/bin/bash`,
`ls /tmp`,
`if [ ! -d "/tmp/build" ]; then`,
` mv /tmp/build .`, // notice the two spaces before 'mv ..'
` ls`, // notice the two spaces before 'ls ..'
`fi`,
),
});
expressions
.expn()
Returns the expression string ${{ <expression> }}
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.expn('hashFiles("**/pnpm-lock.yaml")'))
// '${{ hashFiles("**/pnpm-lock.yaml") }}'
.env()
Returns the expression string ${{ env.SOMETHING }}
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.env('GITHUB_SHA'))
// '${{ env.GITHUB_SHA }}'
.secret()
Returns the expression string ${{ secrets.SOMETHING }}
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.secret('GITHUB_TOKEN'))
// '${{ secrets.GITHUB_TOKEN }}'
.var()
Returns the expression string ${{ vars.SOMETHING }}
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.var('SENTRY_APP_ID'))
// '${{ vars.SENTRY_APP_ID }}'
.ternary()
import { expressions } from 'github-actions-workflow-ts'
// ternary(condition, ifTrue, ifFalse)
console.log(expressions.ternary("github.event_name == 'release'", 'prod', 'dev'))
// '${{ github.event_name == 'release' && 'prod' || 'dev' }}'
echoKeyValue
.to()
Returns the string echo "key=value" >> <SOMETHING>
import { echoKeyValue } from 'github-actions-workflow-ts'
// echoKeyValue.to(key, value, to) returns 'echo "key=value" >> <SOMETHING>'
echoKeyValue.to('@your-org:registry', 'https://npm.pkg.github.com', '.npmrc')
// 'echo "@your-org:registry=https://npm.pkg.github.com" >> .npmrc'
.toGithubEnv()
Returns the string echo "key=value" >> $GITHUB_ENV
import { echoKeyValue } from 'github-actions-workflow-ts'
// echoKeyValue.toGithubEnv(key, value, to) returns 'echo "key=value" >> $GITHUB_ENV'
echoKeyValue.toGithubEnv('NODE_VERSION', '18')
// 'echo "NODE_VERSION=18" >> $GITHUB_ENV'
.toGithubOutput()
Returns the string echo "key=value" >> $GITHUB_OUTPUT
import { echoKeyValue } from 'github-actions-workflow-ts'
// echoKeyValue.toGithubOutput(key, value, to) returns 'echo "key=value" >> $GITHUB_OUTPUT'
echoKeyValue.toGithubOutput('NODE_VERSION', '18')
// 'echo "NODE_VERSION=18" >> $GITHUB_OUTPUT'
Contributing
See the Contributing Guide
Credits
Inspired by webiny/github-actions-wac which is also the original source of the filename extension (.wac.ts
) used to distinguish the Github Actions YAML workflow TypeScript files. When I hit too many limitations with github-actions-wac
, I decided to create github-actions-workflow-ts
to address those limitations and add a lot more functionality.