emmanuelnk / github-actions-workflow-ts

Write Github Actions workflow files in TypeScript (compiles to YAML)
MIT License
104 stars 3 forks source link
actions cicd github-actions javascript typescript yaml

github-actions-workflow-ts

Stop writing workflows in YAML and use TypeScript instead!

github-actions-workflow-ts-logo

love opensource license npm version Tests coverage issues

Table of Contents

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:

  1. Type Safety: Elevate the confidence in your workflows with the robust type-checking capabilities of TypeScript.
  2. Modularity: Efficiently package and reuse common jobs and steps across various workflows, promoting the DRY (Don't Repeat Yourself) principle.
  3. 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.

See more - Install Husky: ```bash npm install --save-dev husky npx husky-init ``` - In `package.json`, add the following script: ```json "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` files ```bash npx husky add .husky/pre-commit "npm run build:workflows" ``` - Now every time you make a change to `*.wac.ts`, Husky will run the `npx 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

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 `` in
the text and it will be replaced with the path to the source-file. | `Array` | # ----DO-NOT-MODIFY-THIS-FILE----
# This file was automatically generated by github-actions-workflow-ts.
# Instead, modify ``
# ----DO-NOT-MODIFY-THIS-FILE---- | | dumpOptions | Options for the dump function of js-yaml. See [all the options here](https://github.com/nodeca/js-yaml#dump-object---options-) | Record | 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.

Example ```ts 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.

Example ```ts 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.

Example ```ts 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

Example ```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 }) testJob.addStep(checkoutStep) ```

.addSteps()

This adds multiple steps to a normal Job

Example ```ts 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

Example ```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 }) 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.

Example ```ts 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.

Example ```ts 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

Example ```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 }) 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

Example ```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 }) 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.

Example ```ts 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
Examples Example 1 ```ts 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 ```yaml - 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: ```ts // 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> }}

Example ```ts 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 }}

Example ```ts import { expressions } from 'github-actions-workflow-ts' console.log(expressions.env('GITHUB_SHA')) // '${{ env.GITHUB_SHA }}' ```

.secret()

Returns the expression string ${{ secrets.SOMETHING }}

Example ```ts import { expressions } from 'github-actions-workflow-ts' console.log(expressions.secret('GITHUB_TOKEN')) // '${{ secrets.GITHUB_TOKEN }}' ```

.var()

Returns the expression string ${{ vars.SOMETHING }}

Example ```ts import { expressions } from 'github-actions-workflow-ts' console.log(expressions.var('SENTRY_APP_ID')) // '${{ vars.SENTRY_APP_ID }}' ```

.ternary()

Example ```ts 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>

Example ```ts import { echoKeyValue } from 'github-actions-workflow-ts' // echoKeyValue.to(key, value, to) returns 'echo "key=value" >> ' 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

Example ```ts 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

Example ```ts 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.