Skip to content

Commit

Permalink
Add support for stubbing @actions/github (#150)
Browse files Browse the repository at this point in the history
This PR adds support for the `@actions/github` package. Specifically,
the goal is to ensure all the needed `GITHUB_*` environment variables
are loaded before the package is imported. Otherwise, creation of the
`Context` class instance happens before the environment variables are
loaded by `dotenv`, causing the `github.context` object to be mostly
empty.

Closes #149
  • Loading branch information
ncalteen authored Feb 6, 2025
2 parents 8146570 + 6dd899e commit fda69c9
Show file tree
Hide file tree
Showing 17 changed files with 477 additions and 11 deletions.
33 changes: 33 additions & 0 deletions __fixtures__/payloads/issue_opened.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"action": "opened",
"issue": {
"assignee": {
"login": "ncalteen"
},
"assignees": [
{
"login": "ncalteen"
}
],
"body": "This is an issue!",
"number": 1,
"state": "open",
"title": "New Issue",
"user": {
"login": "ncalteen"
}
},
"organization": {
"login": "github"
},
"repository": {
"full_name": "github/local-action",
"name": "local-action",
"owner": {
"login": "github"
}
},
"sender": {
"login": "ncalteen"
}
}
73 changes: 73 additions & 0 deletions __tests__/stubs/github/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { jest } from '@jest/globals'
import path from 'path'
import { Context } from '../../../src/stubs/github/context.js'

let envBackup: NodeJS.ProcessEnv

describe('github/context', () => {
beforeEach(() => {
envBackup = process.env
process.env.GITHUB_REPOSITORY = 'github/local-action'
})

afterEach(() => {
process.env = envBackup
jest.resetAllMocks()
})

describe('Context', () => {
it('Creates a Context object', () => {
const context = new Context()
expect(context).toBeInstanceOf(Context)
})

it('Gets the event payload', () => {
process.env.GITHUB_EVENT_PATH = path.join(
process.cwd(),
'__fixtures__',
'payloads',
'issue_opened.json'
)

const context = new Context()
expect(context.payload.action).toBe('opened')
})

it('Does not get the event payload if the path does not exist', () => {
process.env.GITHUB_EVENT_PATH = path.join(
process.cwd(),
'__fixtures__',
'payloads',
'does_not_exist.json'
)

const context = new Context()
expect(context.payload.action).toBeUndefined()
})

it('Gets the issue payload', () => {
process.env.GITHUB_EVENT_PATH = path.join(
process.cwd(),
'__fixtures__',
'payloads',
'issue_opened.json'
)

const context = new Context()
expect(context.issue.number).toBe(1)
})

it('Gets the repo payload', () => {
process.env.GITHUB_EVENT_PATH = path.join(
process.cwd(),
'__fixtures__',
'payloads',
'issue_opened.json'
)

const context = new Context()
expect(context.repo.owner).toBe('github')
expect(context.repo.repo).toBe('local-action')
})
})
})
15 changes: 15 additions & 0 deletions __tests__/stubs/github/github.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { jest } from '@jest/globals'
import * as github from '../../../src/stubs/github/github.js'
import { GitHub } from '../../../src/stubs/github/utils.js'

describe('github/github', () => {
afterEach(() => {
jest.resetAllMocks()
})

describe('getOctokit', () => {
it('Returns the options', () => {
expect(github.getOctokit('test')).toBeInstanceOf(GitHub)
})
})
})
16 changes: 16 additions & 0 deletions __tests__/stubs/github/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { jest } from '@jest/globals'
import * as utils from '../../../src/stubs/github/utils.js'

describe('github/utils', () => {
afterEach(() => {
jest.resetAllMocks()
})

describe('getOctokitOptions', () => {
it('Returns the options', () => {
expect(utils.getOctokitOptions('test')).toEqual({
auth: 'token test'
})
})
})
})
10 changes: 9 additions & 1 deletion docs/supported-functionality.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ to the `local-action` command.
| `toPlatformPath()` | :white_check_mark: | |
| `platform.*` | :white_check_mark: | |

## [`@actions/github`](https://github.com/actions/toolkit/tree/main/packages/github)

The stubbed version of `@actions/github` functions the same as the real package.
However, the functionality is stubbed in order to ensure that all needed
environment variables are pulled from the `.env` file passed to the
`local-action` command. Otherwise, things like `github.context.eventName` will
be `undefined`. For more information, see
[#149](https://github.com/github/local-action/issues/149).

## Under Investigation

The following packages are under investigation for how to integrate with
Expand All @@ -85,7 +94,6 @@ this doesn't work correctly, please
[open an issue!](https://github.com/github/local-action/issues/new)

- [`@actions/exec`](https://github.com/actions/toolkit/tree/main/packages/exec)
- [`@actions/github`](https://github.com/actions/toolkit/tree/main/packages/github)
- [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob)
- [`@actions/http-client`](https://github.com/actions/toolkit/tree/main/packages/http-client)
- [`@actions/io`](https://github.com/actions/toolkit/tree/main/packages/io)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@github/local-action",
"description": "Local Debugging for GitHub Actions",
"version": "2.5.1",
"version": "2.6.0",
"type": "module",
"author": "Nick Alteen <[email protected]>",
"private": false,
Expand Down
36 changes: 36 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import quibble from 'quibble'
import { ARTIFACT_STUBS } from '../stubs/artifact/artifact.js'
import { CORE_STUBS, CoreMeta } from '../stubs/core/core.js'
import { EnvMeta } from '../stubs/env.js'
import { Context } from '../stubs/github/context.js'
import { getOctokit } from '../stubs/github/github.ts'
import type { Action } from '../types.js'
import { printTitle } from '../utils/output.js'
import { isESM } from '../utils/package.js'
Expand Down Expand Up @@ -124,6 +126,23 @@ export async function action(): Promise<void> {
// local-action require a different approach depending on if the called action
// is written in ESM.
if (isESM()) {
await quibble.esm(
path.resolve(
dirs.join(path.sep),
'node_modules',
'@actions',
'github',
'lib',
'github.js'
),
{
getOctokit,
// The context object needs to be created **after** the dotenv file is
// loaded. Otherwise, the GITHUB_* environment variables will not be
// available to the action.
context: new Context()
}
)
await quibble.esm(
path.resolve(
dirs.join(path.sep),
Expand Down Expand Up @@ -158,6 +177,23 @@ export async function action(): Promise<void> {

await run()
} else {
quibble(
path.resolve(
dirs.join(path.sep),
'node_modules',
'@actions',
'github',
'lib',
'github.js'
),
{
getOctokit,
// The context object needs to be created **after** the dotenv file is
// loaded. Otherwise, the GITHUB_* environment variables will not be
// available to the action.
context: new Context()
}
)
quibble(
path.resolve(
dirs.join(path.sep),
Expand Down
4 changes: 2 additions & 2 deletions src/stubs/artifact/internal/delete/delete-artifact.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getOctokit } from '@actions/github'
import { defaults as defaultGitHubOptions } from '@actions/github/lib/utils.js'
import type { OctokitOptions } from '@octokit/core'
import { requestLog } from '@octokit/plugin-request-log'
import { retry } from '@octokit/plugin-retry'
import fs from 'fs'
import path from 'path'
import { EnvMeta } from '../../../../stubs/env.js'
import { getOctokit } from '../../../../stubs/github/github.ts'
import * as core from '../../../core/core.js'
import { defaults as defaultGitHubOptions } from '../../../github/utils.js'
import { getArtifactPublic } from '../find/get-artifact.js'
import { getRetryOptions } from '../find/retry-options.js'
import {
Expand Down
2 changes: 1 addition & 1 deletion src/stubs/artifact/internal/download/download-artifact.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getOctokit } from '@actions/github'
import * as httpClient from '@actions/http-client'
import fs from 'fs'
import path from 'path'
import { finished } from 'stream/promises'
import unzip from 'unzip-stream'
import { EnvMeta } from '../../../../stubs/env.js'
import * as core from '../../../core/core.js'
import { getOctokit } from '../../../github/github.js'
import { getGitHubWorkspaceDir } from '../shared/config.js'
import { ArtifactNotFoundError } from '../shared/errors.js'
import type {
Expand Down
4 changes: 2 additions & 2 deletions src/stubs/artifact/internal/find/get-artifact.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getOctokit } from '@actions/github'
import { defaults as defaultGitHubOptions } from '@actions/github/lib/utils.js'
import type { OctokitOptions } from '@octokit/core'
import { requestLog } from '@octokit/plugin-request-log'
import { retry } from '@octokit/plugin-retry'
import { EnvMeta } from '../../../../stubs/env.js'
import * as core from '../../../core/core.js'
import { getOctokit } from '../../../github/github.js'
import { defaults as defaultGitHubOptions } from '../../../github/utils.js'
import {
ArtifactNotFoundError,
InvalidResponseError
Expand Down
4 changes: 2 additions & 2 deletions src/stubs/artifact/internal/find/list-artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getOctokit } from '@actions/github'
import { defaults as defaultGitHubOptions } from '@actions/github/lib/utils.js'
import type { OctokitOptions } from '@octokit/core'
import { requestLog } from '@octokit/plugin-request-log'
import { retry } from '@octokit/plugin-retry'
import { EnvMeta } from '../../../../stubs/env.js'
import * as core from '../../../core/core.js'
import { getOctokit } from '../../../github/github.js'
import { defaults as defaultGitHubOptions } from '../../../github/utils.js'
import type { Artifact, ListArtifactsResponse } from '../shared/interfaces.js'
import { getUserAgentString } from '../shared/user-agent.js'
import { getRetryOptions } from './retry-options.js'
Expand Down
91 changes: 91 additions & 0 deletions src/stubs/github/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { existsSync, readFileSync } from 'fs'
import { EOL } from 'os'
import { WebhookPayload } from './interfaces.js'

export class Context {
/**
* Webhook payload object that triggered the workflow
*/
payload: WebhookPayload

eventName: string
sha: string
ref: string
workflow: string
action: string
actor: string
job: string
runAttempt: number
runNumber: number
runId: number
apiUrl: string
serverUrl: string
graphqlUrl: string

/**
* Hydrate the context from the environment
*/
constructor() {
this.payload = {}

if (process.env.GITHUB_EVENT_PATH) {
console.log(process.env.GITHUB_EVENT_PATH)
if (existsSync(process.env.GITHUB_EVENT_PATH)) {
this.payload = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH, { encoding: 'utf8' })
)
} else {
const path = process.env.GITHUB_EVENT_PATH
process.stdout.write(`GITHUB_EVENT_PATH ${path} does not exist${EOL}`)
}
}

this.eventName = process.env.GITHUB_EVENT_NAME as string
this.sha = process.env.GITHUB_SHA as string
this.ref = process.env.GITHUB_REF as string
this.workflow = process.env.GITHUB_WORKFLOW as string
this.action = process.env.GITHUB_ACTION as string
this.actor = process.env.GITHUB_ACTOR as string
this.job = process.env.GITHUB_JOB as string
this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT as string, 10)
this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER as string, 10)
this.runId = parseInt(process.env.GITHUB_RUN_ID as string, 10)
/* istanbul ignore next */
this.apiUrl = process.env.GITHUB_API_URL ?? 'https://api.github.com'
/* istanbul ignore next */
this.serverUrl = process.env.GITHUB_SERVER_URL ?? 'https://github.com'
/* istanbul ignore next */
this.graphqlUrl =
process.env.GITHUB_GRAPHQL_URL ?? 'https://api.github.com/graphql'
}

get issue(): { owner: string; repo: string; number: number } {
const payload = this.payload

/* istanbul ignore next */
return {
...this.repo,
number: (payload.issue || payload.pull_request || payload).number
}
}

get repo(): { owner: string; repo: string } {
if (process.env.GITHUB_REPOSITORY) {
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
return { owner, repo }
}

/* istanbul ignore next */
if (this.payload.repository) {
return {
owner: this.payload.repository.owner.login,
repo: this.payload.repository.name
}
}

/* istanbul ignore next */
throw new Error(
"context.repo requires a GITHUB_REPOSITORY environment variable like 'owner/repo'"
)
}
}
Loading

0 comments on commit fda69c9

Please sign in to comment.