1
0
Fork 0
toolkit/docs/github-package.md

10 KiB

Creating an Action using the GitHub Context

Goal

In this walkthrough we will learn how to build a basic action using GitHub context data to greet users when they open an issue or PR. In the process we will explore how to access this context and how to make authenticated requests to the GitHub API.

Note that a complete version of this action can be found at https://github.com/damccorm/issue-greeter.

Prerequisites

This walkthrough assumes that you have gone through the basic javascript action walkthrough and have a basic action set up. If not, we recommend you go through that first.

Installing dependencies

All of the dependencies we need should come packaged for us in this library's core and github packages. To install, run the following in your action:

npm install @actions/core && npm install @actions/github

Metadata

Next, we will need a welcome message and a repo token as an input. Recall that inputs are defined in the action.yml metadata file - update your action.yml file to define welcome-message and repo-token as inputs.

name: 'Welcome'
description: 'A basic welcome action'
author: 'GitHub'
inputs: 
  welcome-message:
    description: 'Message to display when a user opens an issue or PR'
    default: 'Thanks for opening an issue! Make sure you've followed CONTRIBUTING.md'
  repo-token:
    description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
    required: true
runs:
  using: 'node12'
  main: 'lib/main.js'

Action logic

Now that we've installed our dependencies and defined our inputs, we're ready to start writing the action logic in src/main.ts! For clarity, we'll structure our action up as follows:

import * as core from '@actions/core';
import * as github from '@actions/github';

export async function run() {
    try {
    const welcomeMessage: string = core.getInput('welcome-message');
    // TODO - Get context data

    // TODO - make request to the GitHub API to comment on the issue 
    }
    catch (error) {
      core.setFailed(error.message);
      throw error;
    }
}

run();

Getting context data

For the purpose of this walkthrough, we will need the following pieces of context data:

  • the name of the repo that the action is being run on
  • the organization/owner of that repo
  • the number of the issue that has been opened

Fortunately, the GitHub package provides all of this to us with a single convenience function, so we can simply do:

const issue: {owner: string; repo: string; number: number} = github.context.issue;

The context object also contains a number of easily accessed properties, as well as easy access to the full GitHub payload. We can use this to check and make sure we're actually looking at a recently opened issue (and not something else, like a comment on an existing issue):

if (github.context.payload.action !== 'opened') {
  console.log('No issue or PR was opened, skipping');
  return;
}

Our whole src/main.ts file now looks like:

import * as core from '@actions/core';
import * as github from '@actions/github';

export async function run() {
    try {
    const welcomeMessage: string = core.getInput('welcome-message', {required: true});
    const repoToken: string = core.getInput('repo-token', {required: true});
    const issue: {owner: string; repo: string; number: number} = github.context.issue;

    if (github.context.payload.action !== 'opened') {
      console.log('No issue or pull request was opened, skipping');
      return;
    }

    // TODO - make request to the GitHub API to comment on the issue 
    }
    catch (error) {
      core.setFailed(error.message);
      throw error;
    }
}

run();

Sending requests to the GitHub API

Now that we have our context data, we are able to send a request to the GitHub API using the Octokit REST client. The REST client exposes a number of easy convenience functions, including one for adding comments to issues/PRs (issues and PRs are treated as one concept by the Octokit client):

const client: github.GitHub = new github.GitHub(repoToken);
await client.issues.createComment({
  owner: issue.owner,
  repo: issue.repo,
  issue_number: issue.number,
  body: welcomeMessage
});

For more docs on the client, you can visit the Octokit REST documentation. Now our action code should be complete:

import * as core from '@actions/core';
import * as github from '@actions/github';

export async function run() {
    try {
    const welcomeMessage: string = core.getInput('welcome-message', {required: true});
    const repoToken: string = core.getInput('repo-token', {required: true});
    const issue: {owner: string; repo: string; number: number} = github.context.issue;

    if (github.context.payload.action !== 'opened') {
      console.log('No issue or pull request was opened, skipping');
      return;
    }

    const client: github.GitHub = new github.GitHub(repoToken);
    await client.issues.createComment({
      owner: issue.owner,
      repo: issue.repo,
      issue_number: issue.number,
      body: welcomeMessage
    });
    }
    catch (error) {
      core.setFailed(error.message);
      throw error;
    }
}

run();

Writing unit tests for your action

Next, we're going to write a basic unit test for our action using jest. If you followed the javascript walkthrough, you should have a file __tests__/main.test.ts that runs tests when npm test is called. We're going to start by populating that with one test:

const nock = require('nock');
const path = require('path');

describe('action test suite', () => {
  it('It posts a comment on an opened issue', async () => {
    // TODO
  });
});

For the purposes of this walkthrough, we'll focus on populating this test and leave the remaining test coverage as an exercise for the reader.

Mocking inputs

First, we want to make sure that we can mock our inputs (welcome-message, and repo-token). Actions handles inputs by populating process.env.INPUT_${input name in all caps}, so we can mock that simply by setting those environment variables:

const nock = require('nock');
const path = require('path');

describe('action test suite', () => {
  it('It posts a comment on an opened issue', async () => {
    const welcomeMessage = 'hello';
    const repoToken = 'token';
    process.env['INPUT_WELCOME-MESSAGE'] = welcomeMessage;
    process.env['INPUT_REPO-TOKEN'] = repoToken;

    // TODO
  });
});

Mocking the GitHub context

Mocking the GitHub context is relatively straightforward. Since most of it is simply populated by environment variables, you can just set the corresponding environment variables defined here and test that it works in that environment. In this case, we can setup our test with:

const nock = require('nock');
const path = require('path');

describe('action test suite', () => {
  it('It posts a comment on an opened issue', async () => {
    const welcomeMessage = 'hello';
    const repoToken = 'token';
    process.env['INPUT_WELCOME-MESSAGE'] = welcomeMessage;
    process.env['INPUT_REPO-TOKEN'] = repoToken;

    process.env['GITHUB_REPOSITORY'] = 'foo/bar';
    process.env['GITHUB_EVENT_PATH'] = path.join(__dirname, 'payload.json');

    // TODO
  });
});

Note that the payload is loaded from GITHUB_EVENT_PATH. Since we set that to path.join(__dirname, 'payload.json'), we need to go save our payload there. For the purposes of this test, we can simply save the following to __tests__/payload.json:

{
    "issue": {
        "number": 10
    },
    "action": "opened"
}

Now, calling github.context.issue should return {owner: foo, repo: bar, number: 10}, and github.context.payload.action should get set to 'opened'

One important detail here is that because the GitHub context loads these environment variables as soon as it is required, you should set them before you require your action. In most cases, this means you need to rerequire your action in every test. If this is a problem, you can get around it by mocking the class directly using jest (or whatever framework you choose).

Mocking the Octokit Client

To mock the client calls, we recommend using nock which allows you to mock the http requests made by the client. First, install nock with npm install nock --save-dev.

For this test, we expect the following call:

client.issues.createComment({
  owner: 'foo',
  repo: 'bar',
  issue_number: 10,
  body: 'you posted your first issue'
});

From the GitHub endpoint docs, we expect this to get make a POST request to https://api.github.com/repos/foo/bar/issues/10/comments with body of {"body":"hello"}

We can mock this with:

const nock = require('nock');
const path = require('path');

describe('action test suite', () => {
  it('It posts a comment on an opened issue', async () => {
    const welcomeMessage = 'hello';
    const repoToken = 'token';
    process.env['INPUT_WELCOME-MESSAGE'] = welcomeMessage;
    process.env['INPUT_REPO-TOKEN'] = repoToken;

    process.env['GITHUB_REPOSITORY'] = 'foo/bar';
    process.env['GITHUB_EVENT_PATH'] = path.join(__dirname, 'payload.json');

    nock('https://api.github.com')
      .persist()
      .post('/repos/foo/bar/issues/10/comments', '{\"body\":\"hello\"}')
      .reply(200);
      
    const main = require('../src/main');

    await main.run();
  });
});

This will fail if the url or body doesn't exactly match the parameters passed into the nock function. We can now run npm test and the test should succeed.

Build and publish

Now that we've written and unit tested our action, we can build our action with npm run build and push it to a repo where it can be consumed by workflows. For more info on versioning your action, see our versioning docs.

Next steps

If you're interested in building out this action further, try extending your action to only run on a user's first issue. See our first-contribution action for inspiration.