Simplifying W3C standards workflow with a GitHub Action
Tags: github, actionshackathon
Published: 2020-09-14 18:22:23

**What is it about?** In the W3C world, ReSpec and Bikeshed are two common tools being used to write web standards. [ReSpec](https://github.com/w3c/respec/) is a JavaScript based tool that runs in browser and enhances a HTML document to generate a static HTML document that is suitable to be published as a standard. On the other hand, [Bikeshed](https://github.com/tabatkins/bikeshed) is a Python based command line tool which processes a superset of markdown to create similar output. ReSpec also has a CLI. Further, both tools are generic enough to be used (and are being used) outside the W3C. I wrote a bit more about ReSpec in another [post about the ReSpec MOSS Grant](https://dev.to/sidvishnoi/mozilla-open-source-support-grant-helping-make-open-source-sustainable-3540).

**Why a compile step is needed?** ReSpec enhances a HTML document in the browser on runtime, so it is simpler to use (no special tools or dependencies required) and a ReSpec document can be directly hosted on GitHub pages. Bikeshed uses a special text format, so it cannot be hosted directly without a compile step. But as ReSpec does everything at runtime, even with great caching practices, a ReSpec document is slower to load. So, people sometimes use the CLI to compile a ReSpec document and create a static HTML output, as Bikeshed does, and deploy it to GitHub pages.

**Why an Action is needed?** The W3C standards ecosystem is large, and many repositories have setup their own workflows using Travis, GitHub Actions etc. to build, validate and deploy their specs to GitHub Pages or W3C. This leads to fragmentation and it becomes difficult to maintain all these workflows. So, I decided to create a GitHub action, called **Spec Prod**, that can be used by all the specs with minimal effort.

## What does the action do?

When setup in a workflow, the action figures out whether the repository uses Bikeshed or ReSpec, and uses appropriate tools to generate a plain HTML document.

Then it runs various validators, like the W3C markup validator and checks for broken hyperlinks. This serves as a check so we don't accept malformed PRs.

When a pull request is merged, the action can deploy the generated HTML document to GitHub pages. When appropriate, it can also deploy the specification to the W3C website. You can look at a [sample run](https://user-images.githubusercontent.com/8426945/93121389-6376e700-f6e2-11ea-8248-3788d93de5f0.png).

## Developing the action

In this section, I'll share how I developed the action and I hope you can take away a things or two from my experience!

### A composite action

GitHub actions recently started to support "composite" actions, so I wanted to try that. In plain words, you can write multiple small programs/scripts and run them one after other as you run "steps" in a workflow. The composite actions have some work to do on GitHub side, like UI improvements and conditional step execution, but it worked well enough for my use case, with some "hacks".

For the separating steps in the action (not workflow), I decided to use the `group` command. So, all the steps in my action are wrapped in a collapsible section:

```yaml
# action.yml
- run: |
    echo "::group::Step 1"
    run-script-for-step-1
    echo "::endgroup::"
  shell: bash
- run: |
    echo "::group::Step 2"
    run-script-for-step-1
    echo "::endgroup::"
  shell: bash
```

For conditional execution, the scripts do a check and `exit(0)` if that step doesn't need to run. [See example](https://github.com/sidvishnoi/spec-prod/blob/31fbaf287/deploy-gh-pages.js#L11-L14).

It's a bit ugly, but it works.

### Choosing the right language

I love JavaScript. But for this action, I decided to write it in Bash (kinda insane, but I love bash too). Turns out, bash gets out of hands really easily, specially when it comes to string processing and error handling. So, after finishing the "mostly working" action, I decided to re-write it in JavaScript. Some part is still in Bash, perhaps for good ol' memories. Bash is actually great for running sequential commands.
  
Apart from the translation, it didn't require much effort in the action. I had to mostly change the calls like:

```diff
# action.yml
- ${{ github.action_path }}/build.sh
+ node ${{ github.action_path }}/build.sh
```

### Choosing good default inputs

A good GitHub action (or any tool in general) should have a good default behavior. Even when you do not pass any inputs to the [checkout action](https://github.com/actions/checkout/), it does what you expect it to do - it checks out a branch/ref. A good tool should have a good default behavior, and a low learning curve.

The Spec Prod action, also does not require any input. By default, it builds and validates the document. Want to deploy to the GitHub pages branch also? Specify the branch name. Want to deploy to W3C? Authenticate. Should it deploy on PRs? Not likely, so disable all deployments on PRs, unless asked.

### Allow overrides

The default behaviour is not always desirable and a software should be flexible enough when needed. So, the Spec Prod action also allows users to change the behavior. Don't want to run a validator? Tell it not to. The input file does not have a generic name? Specify it.

### Add a pre-process step

As a multi-step action, it helps if you can pre-process all the inputs at once, and pass them to other steps in a "standardized manner". This advice also works in other cases, as long as you don't hit separation of concerns issues.

In a GitHub action, as all inputs are key-value pairs, and it's not cool to ask users to input a stringified JSON value, I decided to prefix input keys with their categories, and pass validated and structured JSON as inputs to internal steps. So, the plain key-value inputs:

```yaml
- uses: sidvishnoi/spec-prod@v1
  with:
    W3C_ECHIDNA_TOKEN: SECRET
    W3C_MANIFEST_URL: URL
    W3C_WG_DECISION_URL: ANOTHER_URL
```

become:

```js
const inputs = {
  // ...
  deploy: {
    w3c: {
      token: "SECRET",
      manifest: "URL",
      decisionUrl: "ANOTHER_URL",
    },
  },
};
```

and we just pass the required inputs a step:

```yaml
- run: node deploy-w3c.js
  env:
    INPUTS_DEPLOY: ${{ toJson(fromJson(steps.prepare.outputs.all).deploy.w3c) }}
```

Also, input names should be descriptive enough that users don't always need to read the full documentation to understand what's going on. So, instead of `W3C_TOKEN`, I used `W3C_ECHIDNA_TOKEN`, as it makes clearer (to the W3C folks) that the token is obtained through [Echidna](https://github.com/w3c/echidna/).

### Keeping it simple - No dependencies

Personally, I don't like build-steps unless required. I like TypeScript, but I didn't write this action in TypeScript because of an seemingly unnecessary (hot take) build step. Instead, I used the `// @ts-check` comment and JSDoc annotations to type-check/hint code within the editor. It works pretty well.

Also, I didn't use the Actions Toolkit, or any other node_modules, and decided to create my own as needed. It's not super productive, but good to have some fun. You don't always need npm; copy-paste works well enough sometimes: compare [this](https://github.com/sidvishnoi/spec-prod/blob/31fbaf28/utils.js#L143-L151) and [this](https://unpkg.com/browse/yn@4.0.0/). Also, in my defense, I [originally](https://github.com/sidvishnoi/spec-prod/tree/d23c9f7ae3bba02cac97229857f4288335b67c88) wrote this entire action in Bash, where npm isn't a thing.

### Deploying to GitHub pages

If you've used GitHub Actions to deploy to GitHub Pages, you must have come across [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages). It's a great action, and does the job.

But there is a catch. GitHub composite actions do not yet allow you to run another GitHub action as a step. It'll be supported in near future, and that would open a lot of opportunities! Also, it is not always super-safe to trust code by others, specially when you pass it a GitHub access token.

I needed to support GitHub pages deployment regardless of that support. So, I decided to rewrite it myself. It took me around [60 lines to Bash](https://github.com/sidvishnoi/spec-prod/blob/d23c9f7a/deploy-gh-pages.sh) to come up with a well working solution. And then later, a near [100 line rewrite in JS](https://github.com/sidvishnoi/spec-prod/blob/main/deploy-gh-pages.js), including more validations and logging. Feel free to copy-paste. Feedback welcome!

### Testing

Testing is presently the weakest part of GitHub actions. Other than simple unit tests, the only way to test the action is to push it, and send PRs/commits to a [test repository](https://github.com/sidvishnoi/w3c-deploy-test). That also means a lot of test runs with commits like: _"please work, does it work now?, done, looks done now, why don't you work!"_ etc.

For some parts, I created a local environment to run the steps with test inputs, but it's still cumbersome.

## What's next?

I will be sending PRs to the repositories that can make use of this action. I've got plenty of [open issues](https://github.com/sidvishnoi/spec-prod/issues/) to work when I get free time. Contributions are welcome!

{% github sidvishnoi/spec-prod no-readme %}