npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

github-actions-workflow-ts

v0.3.0

Published

Generate GitHub Actions workflow files using TypeScript (compiles to YAML)

Downloads

4,636

Readme

github-actions-workflow-ts

Stop writing workflows in YAML and use TypeScript instead!

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.

  • 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 files
    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

| 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.