1
0
Fork 0
toolkit/docs/use-context-in-action.md

11 KiB

Use the GitHub Context in an Action

Goal

In this walkthrough, you 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 make authenticated requests to the GitHub API.

Prerequisites

This walkthrough assumes that you have created a new repository using the actions/typescript-action template and have a basic action set up.

For instructions, check out Creating a repository from a template.

Dependencies

All of the dependencies you need should come from this repository's @actions/core and @actions/github packages.

Install these packages by running the following command from within your action repository:

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

Action Metadata

In order to create a greeting, your action will need two things:

  • A welcome message
  • Permissions to create issue/PR comments

These can be provided to your action as inputs. Inputs are defined in the action.yml metadata file.

Update your action.yml file to define two inputs: welcome-message and repo-token

name: Welcome
description: A basic welcome action
author: GitHub

inputs:
  welcome-message:
    description: A message to display when a user opens an issue or PR
    default: Thanks for opening an issue! Make sure you've followed CONTRIBUTING.md
    required: false
  repo-token:
    description: Token for the repository (e.g. `${{ secrets.GITHUB_TOKEN }}`)
    required: true
runs:
  using: node20
  main: dist/index.js

Action Logic

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

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

export async function run(): Promise<void> {
  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: any) {
    core.setFailed(error.message)
  }
}

Using Context Data

In order to comment on an issue/PR, you will need the following pieces of context data:

  • The name of the repository the action is being run on
  • The organization/owner of that repository
  • The issue or PR number

The @actions/github package provides all of this via the github.context.issue convenience function.

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

The context object contains a number of useful properties, including the full event payload. You can use this to check and make sure this is a recently-opened issue and not something else (like a comment on an existing issue).

if (github.context.action !== 'opened') {
  core.info('No issue or PR was opened, skipping!')
  return
}

The updated src/main.ts file should now look like:

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

export async function run(): Promise<void> {
  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.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: any) {
    core.setFailed(error.message)
    throw error
  }
}

Sending Requests to the GitHub API

Now that you have the context data you need for your action, you can send a request to the GitHub API using the Octokit REST API client. The REST API client exposes a number of convenience functions, including one for adding comments to issues/PRs. For more information about the Octokit client, visit the Octokit documentation.

[!NOTE]

Issues and PRs are treated as one concept by the Octokit client.

const octokit = new github.getOctokit(repoToken)

await octokit.issues.createComment({
  owner: issue.owner,
  repo: issue.repo,
  issue_number: issue.number,
  body: welcomeMessage
})

Your action code should now be complete:

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

export async function run(): Promise<void> {
  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.action !== 'opened') {
      console.log('No issue or pull request was opened, skipping')
      return
    }

    const octokit = github.getOctokit(repoToken)

    await octokit.rest.issues.createComment({
      owner: issue.owner,
      repo: issue.repo,
      issue_number: issue.number,
      body: welcomeMessage
    })
  } catch (error: any) {
    core.setFailed(error.message)
    throw error
  }
}

Writing Unit Tests for your Action

Next, you're going to write a basic unit test for your action using Jest. You should already have a file __tests__/main.test.ts that runs tests when npm test is called.

Remove the contents of that file and replace it with the following:

import * as core from '@actions/core'
import * as github from '@actions/github'
import * as main from '../src/main'

describe('Welcome Message Action', () => {
  it('Posts a comment on an opened issue', async () => {
    // TODO
  })
})

For the purposes of this walkthrough, focus on populating this test and leave the remaining test coverage for later.

Mocking Inputs

First, you will want to make sure that you can mock the return value of the core.getInput function. This will simulate your action getting the welcome-message and repo-token inputs.

import * as core from '@actions/core'
import * as github from '@actions/github'
import * as main from '../src/main'

describe('Welcome Message Action', () => {
  it('Posts a comment on an opened issue', async () => {
    const welcomeMessage = 'Hello, World!'
    const repoToken = 'MY_TOKEN'

    jest
      .spyOn(core, 'getInput')
      .mockReturnValueOnce(welcomeMessage) // The first call to getInput()
      .mockReturnValueOnce(repoToken) // The second call to getInput()

    // TODO
  })
})

Mocking the GitHub Context

You can simulate the GitHub context by mocking the @actions/github package.

import * as core from '@actions/core'
import * as github from '@actions/github'
import * as main from '../src/main'

jest.mock('@actions/github', () => ({
  context: {
    issue: {
      owner: 'octocat',
      repo: 'octo-repo',
      number: 10
    },
    action: 'opened'
  }
}))

describe('Welcome Message Action', () => {
  it('Posts a comment on an opened issue', async () => {
    const welcomeMessage = 'Hello, World!'
    const repoToken = 'MY_TOKEN'

    jest
      .spyOn(core, 'getInput')
      .mockReturnValueOnce(welcomeMessage) // The first call to getInput()
      .mockReturnValueOnce(repoToken) // The second call to getInput()

    // TODO
  })
})

When the test is run, your action code will import @actions/github, receiving the object defined in your test. When your action calls github.context.issue, it should receive the following:

{
  "owner": "octocat",
  "repo": "octo-repo",
  "number": 10
}

Mocking the Octokit Client

You can mock the REST API client simply by expanding the mock you defined in the previous step. Specifically, your mock will need to return a client from the getOctokit function call. If you set this up as a spy instance, you can then test to confirm when and how the client was called. In the following example, mocktokit acts as the mock REST API client.

import * as core from '@actions/core'
import * as github from '@actions/github'
import * as main from '../src/main'

jest.mock('@actions/github', () => ({
  getOctokit: jest.fn(),
  context: {
    issue: {
      owner: 'octocat',
      repo: 'octo-repo',
      number: 10
    },
    action: 'opened'
  }
}))

describe('Welcome Message Action', () => {
  it('Posts a comment on an opened issue', async () => {
    const welcomeMessage = 'Hello, World!'
    const repoToken = 'MY_TOKEN'
    const mocktokit = {
      rest: {
        issues: {
          createComment: jest.fn()
        }
      }
    }

    jest
      .spyOn(core, 'getInput')
      .mockReturnValueOnce(welcomeMessage) // The first call to getInput()
      .mockReturnValueOnce(repoToken) // The second call to getInput()

    // The call to getOctokit()
    jest.spyOn(github, 'getOctokit').mockReturnValue(mocktokit as any)

    // TODO
  })
})

The last step is to call your action and test the outcome. In this case, you will want to make sure that your action posts a comment to the issue. This can be done by checking if createComment was called.

import * as core from '@actions/core'
import * as github from '@actions/github'
import * as main from '../src/main'

jest.mock('@actions/github', () => ({
  getOctokit: jest.fn(),
  context: {
    issue: {
      owner: 'octocat',
      repo: 'octo-repo',
      number: 10
    },
    action: 'opened'
  }
}))

describe('Welcome Message Action', () => {
  it('Posts a comment on an opened issue', async () => {
    const welcomeMessage = 'Hello, World!'
    const repoToken = 'MY_TOKEN'
    const mocktokit = {
      rest: {
        issues: {
          createComment: jest.fn()
        }
      }
    }

    jest
      .spyOn(core, 'getInput')
      .mockReturnValueOnce(welcomeMessage) // The first call to getInput()
      .mockReturnValueOnce(repoToken) // The second call to getInput()

    jest.spyOn(github, 'getOctokit').mockReturnValue(mocktokit as any)

    await main.run()

    expect(mocktokit.rest.issues.createComment).toHaveBeenCalledWith({
      owner: 'octocat',
      repo: 'octo-repo',
      issue_number: 10,
      body: welcomeMessage
    })
  })
})

Build and Publish

Now that you've developed and tested your action, you can build and publish it for others to include in their workflows!

  1. Build the action

    npm run all
    

    This will check the formatting, run linting, perform unit tests, and build the action code.

  2. Commit your changes

  3. Open a pull request

For more info on versioning your action, see Action Versioning.

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 actions/first-interaction for inspiration.