AdaGPT: My Learnings While Building a GitHub Action
Tags: githubhack23, github, typescript, javascript
Published: 2023-05-05 08:06:54

I posted my GitHub Action, [AdaGPT](https://github.com/zirkelc/AdaGPT), for the GitHub Hackathon here on DEV.to a few days ago. While implementing this action, I learned a lot and want to take the time to share them. Here are my learnings in no particular order:

## Action Templates 
To get started quickly with a JavaScript action, I recommend using the official templates from GitHub for [JavaScript](https://github.com/actions/hello-world-javascript-action) and [TypeScript](https://github.com/actions/typescript-action).

## TypeScript Types
Life is easier with static types, at least for me. If you use TypeScript, GitHub provides the [`@octokit/webhooks-types`](https://github.com/octokit/webhooks) package with official type definitions for all of GitHub's [webhooks event types and payloads](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads).

The types are helpful to find out what data is available from the event payload and what data needs to be read with the SDK. For example, the `issue_comment` event for the `created` action contains this data:

```ts
export interface IssueCommentCreatedEvent {
  action: "created";
  /**
   * The [issue](https://docs.github.com/en/rest/reference/issues) the comment belongs to.
   */
  issue: Issue & {
    assignee: User | null;
    /**
     * State of the issue; either 'open' or 'closed'
     */
    state: "open" | "closed";
    locked: boolean;
    labels: Label[];
  };
  comment: IssueComment;
  repository: Repository;
  sender: User;
  installation?: InstallationLite;
  organization?: Organization;
}
```

## Ocktokit Client
The package [`@actions/github`](https://github.com/actions/toolkit/tree/main/packages/github) provides a hydrated [Octokit.js](https://github.com/octokit/octokit.js) client. Octokit.js is the SDK of GitHub and contains several subpackages like [`@octokit/rest`](https://github.com/octokit/rest.js) and [`@octokit/graphql`](https://github.com/octokit/graphql.js) to interact with the REST or GraphQL API.

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

const token = core.getInput('github_token');
const octokit = github.getOctokit(token)

const { data: diff } = await octokit.rest.pulls.get({
    owner: 'octokit',
    repo: 'rest.js',
    pull_number: 123,
    mediaType: {
      format: 'diff'
    }
});
```

The [REST API client](https://github.com/octokit/rest.js) for JavaScript has extensive [documentation](https://octokit.github.io/rest.js/v19) with many code examples. 

## GitHub Context
The package [`@actions/github`](https://github.com/actions/toolkit/tree/main/packages/github) provides a hydrated [`Context`](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts) from the current workflow environment with lots of useful information.   

The current repository and issue number can be retrieved directly from context instead of providing them via an input or reading them from an environment variable:

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

// get issue number from input
const issue = core.getInput('issue_number');

// get repository from environment
const [owner, repo] = (process.env.GITHUB_REPOSITORY || '').split('/');

//---------------------------------------------------------

// get issue number and repository from context
const issue = github.context.issue.number;
const { owner, repo } = github.context.repo;
```

## Comments on Issues and Pull Requests
The `issue_comment` event occurs for comments on both issues **and** pull requests. However, you can use a [conditional check](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only) in the workflow definition to distinguish between issues and pull requests:

```yml
on: issue_comment

jobs:
  pr_commented:
    # This job only runs for pull request comments
    name: PR comment
    if: ${{ github.event.issue.pull_request }}
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo A comment on PR $NUMBER
        env:
          NUMBER: ${{ github.event.issue.number }}

  issue_commented:
    # This job only runs for issue comments
    name: Issue comment
    if: ${{ !github.event.issue.pull_request }}
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo A comment on issue $NUMBER
        env:
          NUMBER: ${{ github.event.issue.number }}
```

The same distinction can be made inside the action with the [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts):

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

// is comment on issue
const isIssueComment = github.context.eventName === 'issue_comment' && github.context.payload.issue?.pull_request === undefined;

// is comment on pull request
const isPullRequestComment = github.context.eventName === 'issue_comment' && github.context.payload.issue?.pull_request !== undefined;
```

## Run Action Locally 
You can use the [`act`](https://github.com/nektos/act) package to run your workflow and action locally so you don't have to commit and push every time you want to test the changes. It works really well and helps you develop much faster.

If you run the workflow locally, you will not get an automatic [GitHub Token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) for interacting with the REST API. This means you need to create a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and provide this token as `GITHUB_TOKEN` for workflow execution. I would recommend creating a local `.env` file with your PAT:

```sh
# file: .env
GITHUB_TOKEN=<personal-access-token>
```

This secret file can be passed to `act` when running locally:

```sh
act issue_comment --secret-file .env
```

As usual, the token is available within the workflow via the syntax `${ secrets.GITHUB_TOKEN }}`.

## Pagination
The REST API is paginated and returns up to 100 items per page. You can use [Pagination API](https://github.com/octokit/octokit.js#pagination) to read all items of a particular endpoint:

```ts
import * as core from '@actions/core'
import * as github from '@actions/github'
import type { Issue } from '@octokit/webhooks-types';

const token = core.getInput('github_token');
const { owner, repo } = github.context.repo;

const octokit = github.getOctokit(token);

const issues: Issue[] = await octokit.paginate(octokit.rest.issues.listForRepo, {
  owner,
  repo,
  per_page: 100,
});
```

## Write Job Summary
The Job Summary is a markdown file with the results of the jobs within a workflow. This [blog post] (https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/) from GitHub gives a good overview. 

For example, I'm writing this job summary for my GitHub Action:

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

await core.summary
  .addLink('Issue', issue.html_url)
  .addHeading('Request', 3)
  .addRaw(request.body ?? '', true)
  .addBreak()
  .addLink('Comment', request.html_url)
  .addHeading('Response', 3)
  .addRaw(response.body ?? '', true)
  .addBreak()
  .addLink('Comment', response.html_url)
  .addBreak()
  .addHeading('GitHub Context', 3)
  .addCodeBlock(JSON.stringify(github.context.payload, null, 2), 'json')
  .write();
```

The rendered markdown looks like this:

![Job Summary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cb1mmjy2wojr3wfp23em.jpg)

[<figcaption>Job Summary</figcaption>](
https://github.com/zirkelc/AdaGPT/actions/runs/4859725206/attempts/1#summary-13167278702)

---

I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you'd like to connect with me, you can find me on [LinkedIn](https://www.linkedin.com/in/christian-zirkel/) or [GitHub](https://github.com/zirkelc). Thanks for reading!