Categories
Tutorials

GitHub Actions: build your own JavaScript action — part 2

Leverage GitHub Issue forms and build your own GitHub Action to automate priority assignment on your issues. This is part 2 of a series of posts on the topic.

In our last post, we built a JavaScript GitHub Action. That automation automatically adds an “Issue Triaged” label to all newly opened issues.

That’s great, but not necessarily very useful. In this second post, let’s look at an example of what we could do to make this a bit more useful!

This post is part of the GitHub Actions: build your own JavaScript action series.

If you’re not following this blog yet, sign up here to get an email as soon as part 4 comes out:

Labels can be useful to categorize and prioritize work in a repo. We can sort our issues:

  • By component; is this about frontend, backend, admin area, editing tools, …
  • By priority; does this bug crash the app and lose all saved data, or is an alignment issue in the Opera browser?
  • By focus area; is this about accessibility, internationalization, performance, user experience, …
  • By size; are we talking about refactoring the whole component or adding a missing semicolon?

That triaging work, although it can be very time-consuming, is a very important task.

Reading each issue, understanding it, reproducing it: you must go through those steps to properly assess the importance of a problem and estimate the impact of a fix. Only then can you prioritize your work in an objective way.

What if we could speed up that process somehow?

Ask precise questions to get detailed answers

Issues are easier to triage when they’re detailed. If the reporter took the time to give some steps to reproduce, you’re halfway there. If they’ve added screenshots and details about the browser they use, most of your work is done!

This is where GitHub’s Issue Forms come in.

With issue and pull request templates, you can customize and standardize the information you’d like contributors to include when they open issues and pull requests in your repository.

Issue and pull request templates

Creating a “Bug Report” issue form for your repository allows you to go from the default issue form to something like this:

The issue form will prompt bug reporters to give you as much information as you need to efficiently triage the issue.

Automate triage with a GitHub Action

Issue forms make for issues with a standard, structured content, something that’s easy to parse by a script. That’s a great opportunity to automate some of our triage work! We can leverage the issue form fields we’ve created to automatically add triage labels to each new issue.

  • If the bug reporter said the problem happens “100% of the time” in the “Chrome” browser, it’s bound to be a higher priority issue than something that happens “sometimes” in the “Opera” browser.
  • If the problem impacts one person, it’s less of an issue than if it impacts every user of your product.
  • If your service is completely unusable because of the bug, it’s more important than an issue that one can work around by clicking elsewhere.

Create a custom issue form with priority-related fields

Let’s start by creating a new issue form, as explained in the docs here, following the syntax described here.

For our example, we’ll use this form, especially the “Issue severity” section:

name: Bug Report
description: Tell me everything. Is the sky falling on your head, or is it just a bug in the code?
labels: [ '[Type] Bug' ]
body:
  - type: textarea
    id: steps
    attributes:
      label: Steps to Reproduce
      placeholder: |
        1. Go to '...'
        2. Click on '....'
        3. Scroll down to '....'
        ...
    validations:
      required: true
  - type: dropdown
    id: users-affected
    attributes:
      label: Severity
      description: How many users are impacted? A guess is fine.
      options:
        - One
        - Some (< 50%)
        - Most (> 50%)
        - All
  - type: dropdown
    id: workarounds
    attributes:
      label: Available workarounds?
      options:
        - No and the platform is unusable
        - No but the platform is still usable
        - Yes, difficult to implement
        - Yes, easy to implement
  - type: textarea
    id: workarounds-detail
    attributes:
      label: Workaround details
      description: If you are aware of a workaround, please describe it below.
      placeholder: |
        e.g. There is an alternative way to access this setting in the sidebar, but it's not readily apparent.

Here is how the issue form will look like for bug reporters:

Let’s build some logic taking into account the 2 dropdown fields, and deciding of the priority of the issue. Here is the matrix I have in mind:

Severity →
Workarounds ↓
AllMost (> 50%)Some (< 50%)One
No and the platform is unusable🏔 High🏔 High🏔 High🏕 Medium
No but the platform is still usable🏕 Medium🏕 Medium🏕 Medium🏕 Medium
Yes, difficult to implement🏕 Medium🏕 Medium🏝 Low🏝 Low
Yes, easy to implement🏕 Medium🏕 Medium🏝 Low🏝 Low
You’re welcome to disagree with the prioritization here. :)

Let’s build a function around this logic, to return a label name based on severity and workaround information:

/**
 * Determine the priority of the issue based on severity and workarounds info from the issue contents.
 * This uses the priority matrix defined in this post: https://jeremy.hu/github-actions-build-javascript-action-part-2/
 *
 * @param {string} severity - How many users are impacted by the problem.
 * @param {string} workaround - Are there any available workarounds for the problem.
 * @returns {string} Priority of issue. High, Medium, Low, or empty string.
 */
function definePriority( severity = '', workaround = '' ) {
	if ( workaround === 'No and the platform is unusable' ) {
		return severity === 'One' ? 'Medium' : 'High';
	} else if ( workaround === 'No but the platform is still usable' ) {
		return 'Medium';
	} else if ( workaround !== '' && workaround !== '_No response_' ) { // "_No response_" is the default value.
		return severity === 'All' || severity === 'Most (> 50%)' ? 'Medium' : 'Low';
	}

	// Fallback.
	return '';
}

Now that we have that logic, we can go back to our GitHub Action. We’ve seen that @actions/github provides us some context about the event that triggered the action. In that context, we can find the body of the issue that was submitted.

In that body, we have structured data submitted via the dropdown fields. Here is an example:

### Severity

Some (< 50%)

### Available workarounds?

Yes, difficult to implement

With a bit of regex, we can extract the severity and the workaround info for our issue. Back in our action’s main file, index.js:

// Extra data from the event, to use in API requests.
const { issue: { number, body }, repository: { owner, name } } = payload;

// List of labels to add to the issue.
const labels = [ 'Issue triaged' ];

// Look for priority indicators in body.
const priorityRegex = /###\sSeverity\n\n(?<severity>.*)\n\n###\sAvailable\sworkarounds\?\n\n(?<workaround>.*)\n/gm;
let match;
while ( ( match = priorityRegex.exec( body ) ) ) {
	const [ , severity = '', workaround = '' ] = match;

	const priorityLabel = definePriority( severity, workaround );
	if ( priorityLabel !== '' ) {
		labels.push( priorityLabel );
	}
}

That’s all there is to it! Commit your changes, push them, and try creating a new issue. You’ll see the priority label being applied for you:

For reference, here is how our action’s index.js file looks like now:

const { setFailed, getInput, debug } = require( '@actions/core' );
const { context, getOctokit } = require( '@actions/github' );

/**
 * Determine the priority of the issue based on severity and workarounds info from the issue contents.
 * This uses the priority matrix defined in this post: https://jeremy.hu/github-actions-build-javascript-action-part-2/
 *
 * @param {string} severity - How many users are impacted by the problem.
 * @param {string} workaround - Are there any available workarounds for the problem.
 * @returns {string} Priority of issue. High, Medium, Low, or empty string.
 */
function definePriority( severity = '', workaround = '' ) {
	if ( workaround === 'No and the platform is unusable' ) {
		return severity === 'One' ? 'Medium' : 'High';
	} else if ( workaround === 'No but the platform is still usable' ) {
		return 'Medium';
	} else if ( workaround !== '' && workaround !== '_No response_' ) { // "_No response_" is the default value.
		return severity === 'All' || severity === 'Most (> 50%)' ? 'Medium' : 'Low';
	}

	// Fallback.
	return '';
}

( async function main() {
	debug( 'Our action is running' );

	const token = getInput( 'github_token' );
	if ( ! token ) {
		setFailed( 'Input `github_token` is required' );
		return;
	}

	// Get the Octokit client.
	const octokit = new getOctokit( token );

	// Get info about the event.
	const { payload, eventName } = context;

	debug( `Received event = '${ eventName }', action = '${ payload.action }'` );

	// We only want to proceed if this is a newly opened issue.
	if ( eventName === 'issues' && payload.action === 'opened' ) {
		// Extra data from the event, to use in API requests.
		const { issue: { number, body }, repository: { owner, name } } = payload;

		// List of labels to add to the issue.
		const labels = [ 'Issue triaged' ];

		// Look for priority indicators in body.
		const priorityRegex = /###\sSeverity\n\n(?<severity>.*)\n\n###\sAvailable\sworkarounds\?\n\n(?<workaround>.*)\n/gm;
		let match;
		while ( ( match = priorityRegex.exec( body ) ) ) {
			const [ , severity = '', workaround = '' ] = match;

			const priorityLabel = definePriority( severity, workaround );
			if ( priorityLabel !== '' ) {
				labels.push( priorityLabel );
			}
		}

		debug(
			`Add the following labels to issue #${ number }: ${ labels
				.map( ( label ) => `"${ label }"` )
				.join( ', ' ) }`
		);

		// Finally make the API request.
		await octokit.rest.issues.addLabels( {
			owner: owner.login,
			repo: name,
			issue_number: number,
			labels,
		} );
	}
} )();

Build your own logic

This was just an example. As I mentioned above, there are different ways to categorize and prioritize work in your repository. We must all consider different factors depending on what we build.

However, we’ve built our GitHub Action so it can be easily customized to fit our exact needs, to match our own logic.

Give this a try, and let me know how it went! In a future post, we’ll look at other things we can do with our shiny Action!

This post is part of the GitHub Actions: build your own JavaScript action series.

If you’re not following this blog yet, sign up here to get an email as soon as part 4 comes out: