diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 06827f277..fe36d9623 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -154,22 +154,58 @@ jobs: return endpoint({}).headers['user-agent'] result-encoding: string - run: | + matches_user_agent() { + local actual="$1" + local prefix="$2" + [[ "$actual" =~ ^${prefix}(\ actions_orchestration_id/[^[:space:]]+)?\ octokit-core\.js/ ]] + } + echo "- Validating user-agent default" - expected="actions/github-script octokit-core.js/" - if [[ "${{steps.user-agent-default.outputs.result}}" != "$expected"* ]]; then - echo $'::error::\u274C' "Expected user-agent to start with '$expected', got ${{steps.user-agent-default.outputs.result}}" + expected="actions/github-script" + if ! matches_user_agent "${{steps.user-agent-default.outputs.result}}" "$expected"; then + echo $'::error::\u274C' "Expected user-agent to start with '$expected' and include 'octokit-core.js/', got ${{steps.user-agent-default.outputs.result}}" exit 1 fi echo "- Validating user-agent set to a value" - expected="foobar octokit-core.js/" - if [[ "${{steps.user-agent-set.outputs.result}}" != "$expected"* ]]; then - echo $'::error::\u274C' "Expected user-agent to start with '$expected', got ${{steps.user-agent-set.outputs.result}}" + expected="foobar" + if ! matches_user_agent "${{steps.user-agent-set.outputs.result}}" "$expected"; then + echo $'::error::\u274C' "Expected user-agent to start with '$expected' and include 'octokit-core.js/', got ${{steps.user-agent-set.outputs.result}}" exit 1 fi echo "- Validating user-agent set to an empty string" - expected="actions/github-script octokit-core.js/" - if [[ "${{steps.user-agent-empty.outputs.result}}" != "$expected"* ]]; then - echo $'::error::\u274C' "Expected user-agent to start with '$expected', got ${{steps.user-agent-empty.outputs.result}}" + expected="actions/github-script" + if ! matches_user_agent "${{steps.user-agent-empty.outputs.result}}" "$expected"; then + echo $'::error::\u274C' "Expected user-agent to start with '$expected' and include 'octokit-core.js/', got ${{steps.user-agent-empty.outputs.result}}" + exit 1 + fi + echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY + + test-get-octokit: + name: 'Integration test: getOctokit with token' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-dependencies + - id: secondary-client + name: Create a second client with getOctokit + uses: ./ + env: + APP_TOKEN: ${{ github.token }} + with: + script: | + const appOctokit = getOctokit(process.env.APP_TOKEN) + const {data} = await appOctokit.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo + }) + + return `${appOctokit !== github}:${data.full_name}` + result-encoding: string + - run: | + echo "- Validating secondary client output" + expected="true:actions/github-script" + if [[ "${{steps.secondary-client.outputs.result}}" != "$expected" ]]; then + echo $'::error::\u274C' "Expected '$expected', got ${{steps.secondary-client.outputs.result}}" exit 1 fi echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY diff --git a/README.md b/README.md index 3dfe48d4e..0cef83cb5 100644 --- a/README.md +++ b/README.md @@ -504,6 +504,8 @@ The `GITHUB_TOKEN` used by default is scoped to the current repository, see [Aut If you need access to a different repository or an API that the `GITHUB_TOKEN` doesn't have permissions to, you can provide your own [PAT](https://help.github.com/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) as a secret using the `github-token` input. +If you need to use multiple tokens in the same script, `getOctokit` is also available in the script context so you can create additional authenticated clients without using `require('@actions/github')`. + [Learn more about creating and using encrypted secrets](https://docs.github.com/actions/reference/encrypted-secrets) ```yaml @@ -516,6 +518,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/github-script@v8 + env: + APP_TOKEN: ${{ secrets.MY_OTHER_PAT }} with: github-token: ${{ secrets.MY_PAT }} script: | @@ -525,6 +529,13 @@ jobs: repo: context.repo.repo, labels: ['Triage'] }) + + const appOctokit = getOctokit(process.env.APP_TOKEN) + await appOctokit.rest.repos.createDispatchEvent({ + owner: 'my-org', + repo: 'another-repo', + event_type: 'trigger-deploy' + }) ``` ### Using exec package diff --git a/__test__/async-function.test.ts b/__test__/async-function.test.ts index d68b3023e..ffa0bedda 100644 --- a/__test__/async-function.test.ts +++ b/__test__/async-function.test.ts @@ -8,6 +8,94 @@ describe('callAsyncFunction', () => { expect(result).toEqual('bar') }) + test('passes getOctokit through the script context', async () => { + const getOctokit = jest.fn().mockReturnValue('secondary-client') + + const result = await callAsyncFunction( + {getOctokit} as any, + "return getOctokit('token')" + ) + + expect(getOctokit).toHaveBeenCalledWith('token') + expect(result).toEqual('secondary-client') + }) + + test('getOctokit creates client independent from github', async () => { + const github = {rest: {issues: 'primary'}} + const getOctokit = jest.fn().mockReturnValue({rest: {issues: 'secondary'}}) + + const result = await callAsyncFunction( + {github, getOctokit} as any, + ` + const secondary = getOctokit('other-token') + return { + primary: github.rest.issues, + secondary: secondary.rest.issues, + different: github !== secondary + } + ` + ) + + expect(result).toEqual({ + primary: 'primary', + secondary: 'secondary', + different: true + }) + expect(getOctokit).toHaveBeenCalledWith('other-token') + }) + + test('getOctokit passes options through', async () => { + const getOctokit = jest.fn().mockReturnValue('client-with-opts') + + const result = await callAsyncFunction( + {getOctokit} as any, + `return getOctokit('my-token', { baseUrl: 'https://ghes.example.com/api/v3' })` + ) + + expect(getOctokit).toHaveBeenCalledWith('my-token', { + baseUrl: 'https://ghes.example.com/api/v3' + }) + expect(result).toEqual('client-with-opts') + }) + + test('getOctokit supports plugins', async () => { + const getOctokit = jest.fn().mockReturnValue('client-with-plugins') + + const result = await callAsyncFunction( + {getOctokit} as any, + `return getOctokit('my-token', { previews: ['v3'] }, 'pluginA', 'pluginB')` + ) + + expect(getOctokit).toHaveBeenCalledWith( + 'my-token', + {previews: ['v3']}, + 'pluginA', + 'pluginB' + ) + expect(result).toEqual('client-with-plugins') + }) + + test('multiple getOctokit calls produce independent clients', async () => { + const getOctokit = jest + .fn() + .mockReturnValueOnce({id: 'client-a'}) + .mockReturnValueOnce({id: 'client-b'}) + + const result = await callAsyncFunction( + {getOctokit} as any, + ` + const a = getOctokit('token-a') + const b = getOctokit('token-b') + return { a: a.id, b: b.id, different: a !== b } + ` + ) + + expect(getOctokit).toHaveBeenCalledTimes(2) + expect(getOctokit).toHaveBeenNthCalledWith(1, 'token-a') + expect(getOctokit).toHaveBeenNthCalledWith(2, 'token-b') + expect(result).toEqual({a: 'client-a', b: 'client-b', different: true}) + }) + test('throws on ReferenceError', async () => { expect.assertions(1) diff --git a/__test__/getoctokit-integration.test.ts b/__test__/getoctokit-integration.test.ts new file mode 100644 index 000000000..8475cf0a1 --- /dev/null +++ b/__test__/getoctokit-integration.test.ts @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import {getOctokit} from '@actions/github' +import {callAsyncFunction} from '../src/async-function' + +describe('getOctokit integration via callAsyncFunction', () => { + test('real getOctokit creates a functional Octokit client in script scope', async () => { + const result = await callAsyncFunction( + {getOctokit} as any, + ` + const client = getOctokit('fake-token-for-test') + return { + hasRest: typeof client.rest === 'object', + hasGraphql: typeof client.graphql === 'function', + hasRequest: typeof client.request === 'function', + hasIssues: typeof client.rest.issues === 'object', + hasPulls: typeof client.rest.pulls === 'object' + } + ` + ) + + expect(result).toEqual({ + hasRest: true, + hasGraphql: true, + hasRequest: true, + hasIssues: true, + hasPulls: true + }) + }) + + test('secondary client is independent from primary github client', async () => { + const primary = getOctokit('primary-token') + + const result = await callAsyncFunction( + {github: primary, getOctokit} as any, + ` + const secondary = getOctokit('secondary-token') + return { + bothHaveRest: typeof github.rest === 'object' && typeof secondary.rest === 'object', + areDistinct: github !== secondary + } + ` + ) + + expect(result).toEqual({ + bothHaveRest: true, + areDistinct: true + }) + }) + + test('getOctokit accepts options for GHES base URL', async () => { + const result = await callAsyncFunction( + {getOctokit} as any, + ` + const client = getOctokit('fake-token', { + baseUrl: 'https://ghes.example.com/api/v3' + }) + return typeof client.rest === 'object' + ` + ) + + expect(result).toBe(true) + }) + + test('multiple getOctokit calls produce independent clients with different tokens', async () => { + const result = await callAsyncFunction( + {getOctokit} as any, + ` + const clientA = getOctokit('token-a') + const clientB = getOctokit('token-b') + return { + aHasRest: typeof clientA.rest === 'object', + bHasRest: typeof clientB.rest === 'object', + areDistinct: clientA !== clientB + } + ` + ) + + expect(result).toEqual({ + aHasRest: true, + bHasRest: true, + areDistinct: true + }) + }) +}) diff --git a/dist/index.js b/dist/index.js index 19ad994c3..4dd053965 100644 --- a/dist/index.js +++ b/dist/index.js @@ -36289,6 +36289,7 @@ async function main() { __original_require__: require, github, octokit: github, + getOctokit: lib_github.getOctokit, context: lib_github.context, core: core, exec: exec, diff --git a/src/async-function.ts b/src/async-function.ts index 84035f222..7f928b4ca 100644 --- a/src/async-function.ts +++ b/src/async-function.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core' import * as exec from '@actions/exec' +import {getOctokit} from '@actions/github' import {Context} from '@actions/github/lib/context' import {GitHub} from '@actions/github/lib/utils' import * as glob from '@actions/glob' @@ -12,6 +13,7 @@ export declare type AsyncFunctionArguments = { core: typeof core github: InstanceType octokit: InstanceType + getOctokit: typeof getOctokit exec: typeof exec glob: typeof glob io: typeof io diff --git a/src/main.ts b/src/main.ts index cbf65693c..a01e1b74e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -66,6 +66,7 @@ async function main(): Promise { __original_require__: __non_webpack_require__, github, octokit: github, + getOctokit, context, core, exec, diff --git a/types/async-function.d.ts b/types/async-function.d.ts index b204e3639..3c2de8e28 100644 --- a/types/async-function.d.ts +++ b/types/async-function.d.ts @@ -1,6 +1,7 @@ /// import * as core from '@actions/core'; import * as exec from '@actions/exec'; +import { getOctokit } from '@actions/github'; import { Context } from '@actions/github/lib/context'; import { GitHub } from '@actions/github/lib/utils'; import * as glob from '@actions/glob'; @@ -10,6 +11,7 @@ export declare type AsyncFunctionArguments = { core: typeof core; github: InstanceType; octokit: InstanceType; + getOctokit: typeof getOctokit; exec: typeof exec; glob: typeof glob; io: typeof io;