From 00afa0fed20254e2eee8e4ac475eb7afbaeb25df Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Sun, 1 Mar 2026 00:25:52 +0000 Subject: [PATCH 1/7] feat: expose `getOctokit` in script context for multi-token workflows --- src/async-function.ts | 6 ++++++ src/main.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/src/async-function.ts b/src/async-function.ts index 84035f222..56fb2398a 100644 --- a/src/async-function.ts +++ b/src/async-function.ts @@ -4,6 +4,7 @@ import {Context} from '@actions/github/lib/context' import {GitHub} from '@actions/github/lib/utils' import * as glob from '@actions/glob' import * as io from '@actions/io' +import type {OctokitOptions, OctokitPlugin} from '@octokit/core/types' const AsyncFunction = Object.getPrototypeOf(async () => null).constructor @@ -12,6 +13,11 @@ export declare type AsyncFunctionArguments = { core: typeof core github: InstanceType octokit: InstanceType + getOctokit: ( + token: string, + options?: OctokitOptions, + ...additionalPlugins: OctokitPlugin[] + ) => InstanceType 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, From a7dc0e4fc1f13a563d93a9fdb7f827f9e8fd69ce Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Sun, 1 Mar 2026 01:30:49 +0000 Subject: [PATCH 2/7] fix: use typeof getOctokit instead of deep @octokit/core import, rebuild dist/ --- src/async-function.ts | 8 ++------ types/async-function.d.ts | 2 ++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/async-function.ts b/src/async-function.ts index 56fb2398a..7f928b4ca 100644 --- a/src/async-function.ts +++ b/src/async-function.ts @@ -1,10 +1,10 @@ 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' import * as io from '@actions/io' -import type {OctokitOptions, OctokitPlugin} from '@octokit/core/types' const AsyncFunction = Object.getPrototypeOf(async () => null).constructor @@ -13,11 +13,7 @@ export declare type AsyncFunctionArguments = { core: typeof core github: InstanceType octokit: InstanceType - getOctokit: ( - token: string, - options?: OctokitOptions, - ...additionalPlugins: OctokitPlugin[] - ) => InstanceType + getOctokit: typeof getOctokit exec: typeof exec glob: typeof glob io: typeof io 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; From cc685dce52aca634bc157fee2f6d4f91f7a8070e Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Mon, 9 Mar 2026 04:44:59 -0700 Subject: [PATCH 3/7] Add docs and tests for getOctokit script context --- README.md | 11 +++++++++++ __test__/async-function.test.ts | 12 ++++++++++++ dist/index.js | 1 + 3 files changed, 24 insertions(+) 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..c37ca5263 100644 --- a/__test__/async-function.test.ts +++ b/__test__/async-function.test.ts @@ -8,6 +8,18 @@ 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('throws on ReferenceError', async () => { expect.assertions(1) 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, From 7cc36cac464d3a25c6542370022040ebb14c6b19 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Mon, 9 Mar 2026 04:52:00 -0700 Subject: [PATCH 4/7] Fix user-agent integration test for orchestration ID --- .github/workflows/integration.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 06827f277..ef087470d 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -154,22 +154,28 @@ 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 From 74a7682fdb4a38384d84c5c7639ffb01329db937 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Mon, 9 Mar 2026 05:01:43 -0700 Subject: [PATCH 5/7] Add integration test for getOctokit client creation --- .github/workflows/integration.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index ef087470d..fe36d9623 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -180,6 +180,36 @@ jobs: 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 + test-debug: strategy: matrix: From 2fe016f5e5233872adf342bc4fbe5b88a5a492cd Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Tue, 7 Apr 2026 15:46:47 +0000 Subject: [PATCH 6/7] test: add multi-token usage tests for getOctokit --- __test__/async-function.test.ts | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/__test__/async-function.test.ts b/__test__/async-function.test.ts index c37ca5263..ffa0bedda 100644 --- a/__test__/async-function.test.ts +++ b/__test__/async-function.test.ts @@ -20,6 +20,82 @@ describe('callAsyncFunction', () => { 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) From 744020488d95dd477221301c34aca614e8725619 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Tue, 7 Apr 2026 16:02:54 +0000 Subject: [PATCH 7/7] test: add getOctokit integration tests for multi-token usage Verifies the real getOctokit from @actions/github creates functional Octokit clients when invoked through callAsyncFunction, ensuring: - Secondary clients have full REST/GraphQL API surface - Secondary clients are independent from primary github client - GHES base URL option is accepted - Multiple tokens produce distinct client instances --- __test__/getoctokit-integration.test.ts | 85 +++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 __test__/getoctokit-integration.test.ts 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 + }) + }) +})