Create TypeSafe GitHub actions
Continuous integration (CI) is an essential safety net for any sustainable open source project. GitHub Actions have become the industry standard. One downside is the yaml configuration format. .yml
files can be error-prone and make reuse more difficult.
I decided to create this project after implementing a rust build pipeline for a rust
/ napi
project which needed to be compiled across multiple architectures. The .yml
files were complex and hard to maintain. By using this tool I've been able to reduce the complexity massively and allow for much simpler reuse of code.
This project requires a recent installation of deno
and is tested to run on versions greater than 1.24.x
. It may work on older versions but this is not guaranteed.
There are two ways of running this project:
deno install -Af --name actionify https://deno.land/x/actionify@0.3.0/cli.ts
After this is run you will be able to run actionify
from the command line.
actionify # Autogenerates all the workflows using defaults
actionify --help # Prints the help menu
actionify check # Checks whether workflows are up to date and valid
deno run -Ar https://deno.land/x/actionify@0.3.0/cli.ts
If you are using vscode
you can install the deno extension which will provide you with syntax highlighting and autocompletion.
Enabling deno
for the whole project may cause interoperability issues if you are using a default TypeScript setup. The best way to get around this is to create a .vscode/settings.json
file in the root of your project and add the following:
{
"deno.enablePaths": [".github/"]
}
This restricts deno
support to the .github
folder and will fix most of the conflicts. Depending on your TypeScript setup you may also need to exclude the .github
folder via your tsconfig.json
file.
{
"compilerOptions": {
// ...
},
"exclude": [".github/**"]
}
The following setup uses the defaults. These can be customised as documented later in this readme.
The first thing to do is create the file .github/actionify.ts
file which will contain the configuration for creating all workflow actions. The example will be taken from the GitHub quickstart guide.
import {
defineWorkflows,
e,
step,
workflow,
} from "https://deno.land/x/actionify@0.3.0/mod.ts";
// Create the configuration for the CI workflow.
const ciWorkflow = workflow({ name: "GitHub Actions Demo", fileName: "ci" })
.on("push")
// Set the job and it's ID. The ID can be used to access the job context later.
.job("Explore-GitHub-Actions", (job) => {
return job.steps(...generateSteps());
});
// Create the configuration for the CI workflows. When the CLI is run this will be
// used to create the workflows.
export default defineWorkflows({
workflows: [ciWorkflow],
});
// Create a list of steps which will be used in the job.
function generateSteps() {
return [
step().run(
`echo "π The job was automatically triggered by a ${
e.wrap(e.ctx.github.event_name)
} event.`,
),
step().run(
`echo "π§ This job is now running on a ${
e.wrap(e.ctx.runner.os)
} server hosted by GitHub!"`,
),
step().run(
`echo "π The name of your branch is ${
e.wrap(e.ctx.github.ref)
} and your repository is ${e.wrap(e.ctx.github.repository)}."`,
),
step().name("Check out the repository code").uses("actions/checkout@v3"),
step().run(
`echo "π‘ The ${
e.wrap(e.ctx.github.repository)
} repository has been cloned to the runner."`,
),
step().run(
'echo "π₯οΈ The workflow is now ready to test your code on the runner."',
),
step().name("List files in your repository").run([
`ls ${e.wrap(e.ctx.github.workspace)}`,
]),
step().run((ctx) => {
return `echo "π This job's status is ${e.wrap(ctx.job.status)}."`;
}),
];
}
Once the above example is in place, you can run the following to generate all the workflow files.
actionify
deno run -Ar https://deno.land/x/actionify@0.3.0/cli.ts
This creates the following file: .github/workflows/ci.yml
# This file was autogenerated with actionify@0.0.0
# To update run:
# deno run -Ar https://deno.land/x/actionify@0.3.0/cli.ts
name: GitHub Actions Demo
'on':
push: null
jobs:
Explore-GitHub-Actions:
steps:
- run: echo "π The job was automatically triggered by a ${{ github.event_name }} event.
- run: echo "π§ This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: >-
echo "π The name of your branch is ${{ github.ref }} and your repository is ${{
github.repository }}."
- name: Check out the repository code
uses: actions/checkout@v3
- run: echo "π‘ The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "π₯οΈ The workflow is now ready to test your code on the runner."
- name: List files in your repository
run: ls ${{ github.workspace }}
- run: echo "π This job's status is ${{ job.status }}."
Right now it's possible to create invalid workflow files.
- Users can create a workflow file with no jobs and no events. Both of which are required.
- Users can create a job with no runner (which is required) and no steps.
- Users can create empty steps which also don't fail.
Any of the above will cause the workflow to fail on GitHub.
There are two ways of adding validation.
Validation can be added during:
- workflow generation throw an error when required properties are missing.
- TypeScript: meaning that only valid workflows can be added to
defineWorkflows
, only valid jobs can be added toWorkflow.job
and only valid steps can be added toJob.steps
.
Ideally both of these would be implemented to prevent workflows from failing on GitHub.
It will be important to get community feedback on the TypeScript API. There are a several places that things could be improved, and design choices around making types more strict or making the API more intuitive.
The main reason I created this project is to have type-safe control over GitHub actions. Eventually this will also mean that remote actions can be called in a fully type safe way, without ever needing to leave your code editor.
This could be fulfilled via a deno registry deployment allowing for remote actions to be imported and used in a type-safe way.
import checkout from "https://act.deno.dev/actions/checkout@v3.0.2";
import {
defineWorkflows,
e,
workflow,
} from "https://deno.land/x/actionify@0.3.0/mod.ts";
const checkoutStep = checkout((ctx) => ({
repository: e.wrap(ctx.github.repository),
ref: e.wrap(ctx.github.ref),
token: e.wrap(ctx.github.token),
lfs: true,
})).env((ctx) => ({
GITHUB_TOKEN: e.wrap(ctx.secrets.GITHUB_TOKEN),
}));
const ci = workflow({ name: "ci" })
.on("push")
.job("Explore-GitHub-Actions", (job) => job.step(checkoutStep));
export default defineWorkflows({
workflows: [ci],
});
The remote actions will be dynamically generated and cached.
A user should be able to run write a function that runs directly within the function context. The script created would use deno as the runtime.
import { step } from "https://deno.land/x/actionify@0.3.0/mod.ts";
const scriptStep = step()
// @ts-expect-error
.script(async function (...args: number[]) {
// This runs the script in the context of the step and has access to the deno runtime.
const result = await Deno.readTextFile(
new URL(import.meta.resolve("./readme.md")),
);
// do something with the result
}, [1, 2, 3]);
To contribute first update your cache with
deno task lock
This both generates the lockfile and makes sure the same cache is used for all contributors.
To check that all you code is working as expected, run:
deno task check
This will test, lint and check that formatting is correct.
created with scaffold