Stop writing workflows in YAML and use TypeScript instead!
- github-actions-workflow-ts
npm install --save-dev github-actions-workflow-ts
Or to use the zero dependency no-cli package (if you only want to generate a workflow JSON object and use it in something like projen):
npm install --save-dev github-actions-workflow-ts-lib
Introducing github-actions-workflow-ts
: A seamless integration allowing developers to author GitHub Actions workflows with the power and flexibility of TypeScript.
- 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.
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!
Want to quickly see it in action? Explore these Replit examples (create a free account to fork and modify my 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)
If you want to use the zero dependency package, you can do this:
import { Workflow, NormalJob, Step } from 'github-actions-workflow-ts-lib'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
const testJob = new NormalJob('Test', {
'runs-on': 'ubuntu-latest',
'timeout-minutes': 2
})
const exampleWorkflow = new Workflow('example-filename', {
name: 'Example',
on: {
workflow_dispatch: {}
}
})
// the generated workflow object to be used in other parts of your project
// without conversion to YAML
console.log(exampleWorkflow.workflow)
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
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.
See more
- 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
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
See config options
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 |
The building block of every NormalJob
. Contains instructions on what to run in your Github Actions Runner in each job.
Example
import { Step } from 'github-actions-workflow-ts'
const checkoutStep = new Step({
name: 'Checkout',
uses: 'actions/checkout@v3',
})
This adds environment variables to a step.
Example
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'
})
The most typical job that contains steps.
This adds environment variables to a job.
Example
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'
})
This adds a single step to a normal Job
Example
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)
This adds multiple steps to a normal Job
Example
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
])
This adds any jobs that the current job depends on to the current job's needs
property
Example
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
])
A job that allows you to call another workflow and use it in the same run.
Example
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)
Same as NormalJob.needs()
This adds environment variables to a workflow.
Example
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'
})
This adds a single job to a Workflow
Example
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)
This adds multiple jobs to a Workflow
Example
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
])
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.
These are types generated right out of the Github Actions Workflow JSON Schema
These are types that I extended myself because they weren't autogenerated from the JSON Schema.
Example
import {
Workflow,
NormalJob,
Step,
expressions as ex,
GeneratedWorkflowTypes as GWT, // all types generated from the official Github Actions Workflow JSON Schema
} from '../src'
const nodeSetupStep: GWT.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,
},
})
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
Examples
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`,
),
});
Returns the expression string ${{ <expression> }}
Example
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.expn('hashFiles("**/pnpm-lock.yaml")'))
// '${{ hashFiles("**/pnpm-lock.yaml") }}'
Returns the expression string ${{ env.SOMETHING }}
Example
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.env('GITHUB_SHA'))
// '${{ env.GITHUB_SHA }}'
Returns the expression string ${{ secrets.SOMETHING }}
Example
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.secret('GITHUB_TOKEN'))
// '${{ secrets.GITHUB_TOKEN }}'
Returns the expression string ${{ vars.SOMETHING }}
Example
import { expressions } from 'github-actions-workflow-ts'
console.log(expressions.var('SENTRY_APP_ID'))
// '${{ vars.SENTRY_APP_ID }}'
Example
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' }}'
Returns the string echo "key=value" >> <SOMETHING>
Example
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'
Returns the string echo "key=value" >> $GITHUB_ENV
Example
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'
Returns the string echo "key=value" >> $GITHUB_OUTPUT
Example
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'
See the Contributing Guide
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.