1
0
Fork 0

Add github context getting started docs (#80)

* WIP - Add github context getting started docs

* Add testing docs

* Update walkthrough after going through it

* Clean up

* Add markdown types

* Env disclaimer

* Fix broken link to Javascript Action (#103)
pull/128/head
Danny McCormick 2019-09-09 11:52:50 -04:00 committed by GitHub
parent ef4525e9dd
commit 3dcd65e44b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 367 additions and 78 deletions

View File

@ -1,100 +1,288 @@
# Github Package # Creating an Action using the GitHub Context
In order to support using actions to interact with GitHub, I propose adding a `github` package to the toolkit. ## Goal
Its main purpose will be to provide a hydrated GitHub context/Octokit client with some convenience functions. It is largely pulled from the GitHub utilities provided in https://github.com/JasonEtco/actions-toolkit, though it has been condensed. 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.
### Spec Note that a complete version of this action can be found at https://github.com/damccorm/issue-greeter.
##### interfaces.ts ## Prerequisites
This walkthrough assumes that you have gone through the basic [javascript action walkthrough](./javascript-action.md) 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.
```yaml
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:
```ts ```ts
/* import * as core from '@actions/core';
* Interfaces import * as github from '@actions/github';
*/
export interface PayloadRepository { export async function run() {
[key: string]: any try {
full_name?: string const welcomeMessage: string = core.getInput('welcome-message');
name: string // TODO - Get context data
owner: {
[key: string]: any // TODO - make request to the GitHub API to comment on the issue
login: string }
name?: string catch (error) {
core.setFailed(error.message);
throw error;
} }
html_url?: string
} }
export interface WebhookPayloadWithRepository { run();
[key: string]: any ```
repository?: PayloadRepository
issue?: { ### Getting context data
[key: string]: any
number: number For the purpose of this walkthrough, we will need the following pieces of context data:
html_url?: string
body?: string - the name of the repo that the action is being run on
} - the organization/owner of that repo
pull_request?: { - the number of the issue that has been opened
[key: string]: any
number: number Fortunately, the GitHub package provides all of this to us with [a single convenience function](https://github.com/actions/toolkit/blob/ac007c06984bc483fae2ba649788dfc858bc6a8b/packages/github/src/context.ts#L34), so we can simply do:
html_url?: string
body?: string `const issue: {owner: string; repo: string; number: number} = github.context.issue;`
}
sender?: { The context object also contains a number of easily accessed properties, as well as easy access to the full [GitHub payload](https://developer.github.com/v3/activity/events/types/). 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):
[key: string]: any
type: string ```ts
} if (github.context.payload.action !== 'opened') {
action?: string console.log('No issue or PR was opened, skipping');
installation?: { return;
id: number
[key: string]: any
}
} }
``` ```
##### context.ts Our whole `src/main.ts` file now looks like:
Contains a GitHub context
```ts ```ts
export class Context { import * as core from '@actions/core';
/** import * as github from '@actions/github';
* Webhook payload object that triggered the workflow
*/
public payload: WebhookPayloadWithRepository
/** export async function run() {
* Name of the event that triggered the workflow try {
*/ const welcomeMessage: string = core.getInput('welcome-message', {required: true});
public event: string const repoToken: string = core.getInput('repo-token', {required: true});
public sha: string const issue: {owner: string; repo: string; number: number} = github.context.issue;
public ref: string
public workflow: string
public action: string
public actor: string
/** if (github.context.payload.action !== 'opened') {
* Hydrate the context from the environment console.log('No issue or pull request was opened, skipping');
*/ return;
constructor ()
public get issue ()
public get repo ()
} }
// TODO - make request to the GitHub API to comment on the issue
}
catch (error) {
core.setFailed(error.message);
throw error;
}
}
run();
``` ```
##### github.ts ### Sending requests to the GitHub API
Contains a hydrated Octokit client Now that we have our context data, we are able to send a request to the GitHub API using the [Octokit REST client](https://github.com/octokit/rest.js). 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):
```ts ```ts
export class GithubClient extends Octokit { const client: github.GitHub = new github.GitHub(repoToken);
// For making GraphQL requests await client.issues.createComment({
public graphql: (query: string, variables?: Variables) => Promise<GraphQlQueryResponse> owner: issue.owner,
repo: issue.repo,
issue_number: issue.number,
body: welcomeMessage
});
```
// Calls super and initializes graphql For more docs on the client, you can visit the [Octokit REST documentation](https://octokit.github.io/rest.js/). Now our action code should be complete:
constructor (token: string)
```ts
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](./javascript-action.md), 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:
```ts
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:
```ts
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](https://github.com/actions/toolkit/blob/ac007c06984bc483fae2ba649788dfc858bc6a8b/packages/github/src/context.ts#L23) and test that it works in that environment. In this case, we can setup our test with:
```ts
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`:
```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](https://github.com/nock/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:
```ts
client.issues.createComment({
owner: 'foo',
repo: 'bar',
issue_number: 10,
body: 'you posted your first issue'
});
```
From [the GitHub endpoint docs](https://developer.github.com/v3/issues/comments/#create-a-comment), 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:
```ts
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](./action-versioning.md).
## 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](https://github.com/actions/first-interaction) for inspiration.

View File

@ -159,5 +159,6 @@ steps:
name: World! name: World!
``` ```
# Next Steps
Now that you've created a basic action, see how to [leverage the github context](./github-package) in your actions

View File

@ -0,0 +1,100 @@
# Github Package
In order to support using actions to interact with GitHub, I propose adding a `github` package to the toolkit.
Its main purpose will be to provide a hydrated GitHub context/Octokit client with some convenience functions. It is largely pulled from the GitHub utilities provided in https://github.com/JasonEtco/actions-toolkit, though it has been condensed.
### Spec
##### interfaces.ts
```ts
/*
* Interfaces
*/
export interface PayloadRepository {
[key: string]: any
full_name?: string
name: string
owner: {
[key: string]: any
login: string
name?: string
}
html_url?: string
}
export interface WebhookPayloadWithRepository {
[key: string]: any
repository?: PayloadRepository
issue?: {
[key: string]: any
number: number
html_url?: string
body?: string
}
pull_request?: {
[key: string]: any
number: number
html_url?: string
body?: string
}
sender?: {
[key: string]: any
type: string
}
action?: string
installation?: {
id: number
[key: string]: any
}
}
```
##### context.ts
Contains a GitHub context
```ts
export class Context {
/**
* Webhook payload object that triggered the workflow
*/
public payload: WebhookPayloadWithRepository
/**
* Name of the event that triggered the workflow
*/
public event: string
public sha: string
public ref: string
public workflow: string
public action: string
public actor: string
/**
* Hydrate the context from the environment
*/
constructor ()
public get issue ()
public get repo ()
}
```
##### github.ts
Contains a hydrated Octokit client
```ts
export class GithubClient extends Octokit {
// For making GraphQL requests
public graphql: (query: string, variables?: Variables) => Promise<GraphQlQueryResponse>
// Calls super and initializes graphql
constructor (token: string)
}
```