From a8ea745713d98f479025e95def530e4a616e0420 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Tue, 7 Apr 2026 16:16:11 +0000 Subject: [PATCH 1/4] feat(github): append orchestration ID to user-agent in getOctokitOptions When ACTIONS_ORCHESTRATION_ID is set, appends actions_orchestration_id/{sanitizedId} to the user-agent string. - Add getUserAgentWithOrchestrationId() to internal/utils.ts - Wire into getOctokitOptions() so all getOctokit() calls include it - Re-export helper from @actions/github/lib/utils for downstream consumers - 14 deterministic unit tests covering helper, integration, edge cases --- packages/github/RELEASES.md | 5 + .../github/__tests__/orchestration.test.ts | 118 ++++++++++++++++++ packages/github/src/internal/utils.ts | 12 ++ packages/github/src/utils.ts | 10 ++ 4 files changed, 145 insertions(+) create mode 100644 packages/github/__tests__/orchestration.test.ts diff --git a/packages/github/RELEASES.md b/packages/github/RELEASES.md index d56d82d49c..487e12378c 100644 --- a/packages/github/RELEASES.md +++ b/packages/github/RELEASES.md @@ -1,5 +1,10 @@ # @actions/github Releases +### 9.1.0 + +- Append `actions_orchestration_id` to user-agent when `ACTIONS_ORCHESTRATION_ID` environment variable is set +- Export `getUserAgentWithOrchestrationId` from `@actions/github/lib/utils` for downstream consumers + ### 9.0.0 - **Breaking change**: Package is now ESM-only diff --git a/packages/github/__tests__/orchestration.test.ts b/packages/github/__tests__/orchestration.test.ts new file mode 100644 index 0000000000..35ef91d1d7 --- /dev/null +++ b/packages/github/__tests__/orchestration.test.ts @@ -0,0 +1,118 @@ +import {getOctokitOptions, getUserAgentWithOrchestrationId} from '../src/utils' +import {getUserAgentWithOrchestrationId as internalGetUserAgentWithOrchestrationId} from '../src/internal/utils' + +describe('orchestration ID support', () => { + let originalOrchId: string | undefined + + beforeEach(() => { + originalOrchId = process.env['ACTIONS_ORCHESTRATION_ID'] + delete process.env['ACTIONS_ORCHESTRATION_ID'] + }) + + afterEach(() => { + if (originalOrchId !== undefined) { + process.env['ACTIONS_ORCHESTRATION_ID'] = originalOrchId + } else { + delete process.env['ACTIONS_ORCHESTRATION_ID'] + } + }) + + describe('getUserAgentWithOrchestrationId', () => { + it('returns undefined when env var is not set and no base user agent', () => { + expect(getUserAgentWithOrchestrationId()).toBeUndefined() + }) + + it('returns base user agent unchanged when env var is not set', () => { + expect(getUserAgentWithOrchestrationId('my-app')).toBe('my-app') + }) + + it('returns orchestration ID without base when env var is set and no base', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'abc-123' + expect(getUserAgentWithOrchestrationId()).toBe( + 'actions_orchestration_id/abc-123' + ) + }) + + it('appends orchestration ID to base user agent', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'abc-123' + expect(getUserAgentWithOrchestrationId('my-app')).toBe( + 'my-app actions_orchestration_id/abc-123' + ) + }) + + it('sanitizes special characters in orchestration ID', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'id with spaces/and$pecial!' + expect(getUserAgentWithOrchestrationId('my-app')).toBe( + 'my-app actions_orchestration_id/id_with_spaces_and_pecial_' + ) + }) + + it('preserves allowed characters in orchestration ID', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = + 'valid_id-with.allowed_chars.123' + expect(getUserAgentWithOrchestrationId()).toBe( + 'actions_orchestration_id/valid_id-with.allowed_chars.123' + ) + }) + + it('ignores whitespace-only orchestration ID', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = ' ' + expect(getUserAgentWithOrchestrationId('my-app')).toBe('my-app') + }) + }) + + describe('public re-export', () => { + it('exports getUserAgentWithOrchestrationId from utils (public API)', () => { + expect(getUserAgentWithOrchestrationId).toBe( + internalGetUserAgentWithOrchestrationId + ) + }) + }) + + describe('getOctokitOptions', () => { + it('sets userAgent when ACTIONS_ORCHESTRATION_ID is set', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id' + const opts = getOctokitOptions('fake-token') + expect(opts.userAgent).toBe( + 'actions_orchestration_id/test-orch-id' + ) + }) + + it('does not set userAgent when ACTIONS_ORCHESTRATION_ID is not set', () => { + const opts = getOctokitOptions('fake-token') + expect(opts.userAgent).toBeUndefined() + }) + + it('preserves and appends to caller-provided userAgent', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id' + const opts = getOctokitOptions('fake-token', { + userAgent: 'custom-agent/1.0' + }) + expect(opts.userAgent).toBe( + 'custom-agent/1.0 actions_orchestration_id/test-orch-id' + ) + }) + + it('leaves caller-provided userAgent intact when env var is not set', () => { + const opts = getOctokitOptions('fake-token', { + userAgent: 'custom-agent/1.0' + }) + expect(opts.userAgent).toBe('custom-agent/1.0') + }) + + it('does not mutate the original options object', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id' + const original = {userAgent: 'original/1.0'} + getOctokitOptions('fake-token', original) + expect(original.userAgent).toBe('original/1.0') + }) + + it('sanitizes special characters through getOctokitOptions', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'bad chars here!' + const opts = getOctokitOptions('fake-token') + expect(opts.userAgent).toBe( + 'actions_orchestration_id/bad_chars_here_' + ) + }) + }) +}) diff --git a/packages/github/src/internal/utils.ts b/packages/github/src/internal/utils.ts index c97db1850e..5ff4db52fe 100644 --- a/packages/github/src/internal/utils.ts +++ b/packages/github/src/internal/utils.ts @@ -42,3 +42,15 @@ export function getProxyFetch(destinationUrl): typeof fetch { export function getApiBaseUrl(): string { return process.env['GITHUB_API_URL'] || 'https://api.github.com' } + +export function getUserAgentWithOrchestrationId( + baseUserAgent?: string +): string | undefined { + const orchId = process.env['ACTIONS_ORCHESTRATION_ID']?.trim() + if (orchId) { + const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_') + const ua = baseUserAgent ? `${baseUserAgent} ` : '' + return `${ua}actions_orchestration_id/${sanitizedId}` + } + return baseUserAgent +} diff --git a/packages/github/src/utils.ts b/packages/github/src/utils.ts index e06c4f98d0..d783278d84 100644 --- a/packages/github/src/utils.ts +++ b/packages/github/src/utils.ts @@ -23,6 +23,8 @@ export const GitHub = Octokit.plugin( paginateRest ).defaults(defaults) +export {getUserAgentWithOrchestrationId} from './internal/utils.js' + /** * Convience function to correctly format Octokit Options to pass into the constructor. * @@ -41,5 +43,13 @@ export function getOctokitOptions( opts.auth = auth } + // Orchestration ID + const userAgent = Utils.getUserAgentWithOrchestrationId( + opts.userAgent as string | undefined + ) + if (userAgent) { + opts.userAgent = userAgent + } + return opts } From b0917c5a37118baba90bd114e17dfcf9de8997f2 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Tue, 7 Apr 2026 16:35:32 +0000 Subject: [PATCH 2/4] style: fix prettier formatting in orchestration tests --- packages/github/__tests__/orchestration.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/github/__tests__/orchestration.test.ts b/packages/github/__tests__/orchestration.test.ts index 35ef91d1d7..3b851ddbba 100644 --- a/packages/github/__tests__/orchestration.test.ts +++ b/packages/github/__tests__/orchestration.test.ts @@ -73,9 +73,7 @@ describe('orchestration ID support', () => { it('sets userAgent when ACTIONS_ORCHESTRATION_ID is set', () => { process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id' const opts = getOctokitOptions('fake-token') - expect(opts.userAgent).toBe( - 'actions_orchestration_id/test-orch-id' - ) + expect(opts.userAgent).toBe('actions_orchestration_id/test-orch-id') }) it('does not set userAgent when ACTIONS_ORCHESTRATION_ID is not set', () => { @@ -110,9 +108,7 @@ describe('orchestration ID support', () => { it('sanitizes special characters through getOctokitOptions', () => { process.env['ACTIONS_ORCHESTRATION_ID'] = 'bad chars here!' const opts = getOctokitOptions('fake-token') - expect(opts.userAgent).toBe( - 'actions_orchestration_id/bad_chars_here_' - ) + expect(opts.userAgent).toBe('actions_orchestration_id/bad_chars_here_') }) }) }) From ffeb50bd029ac95a0cf7015ccbba6bcd1d65f44e Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Wed, 8 Apr 2026 16:49:32 +0000 Subject: [PATCH 3/4] fix: prevent duplicate orchestration ID in user-agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add idempotency check to getUserAgentWithOrchestrationId — if the tag is already present in baseUserAgent, return it unchanged. This prevents doubling when both the exported helper and getOctokitOptions run for the same client. --- .../github/__tests__/orchestration.test.ts | 18 ++++++++++++++++++ packages/github/src/internal/utils.ts | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/github/__tests__/orchestration.test.ts b/packages/github/__tests__/orchestration.test.ts index 3b851ddbba..0bda82f0a0 100644 --- a/packages/github/__tests__/orchestration.test.ts +++ b/packages/github/__tests__/orchestration.test.ts @@ -59,6 +59,14 @@ describe('orchestration ID support', () => { process.env['ACTIONS_ORCHESTRATION_ID'] = ' ' expect(getUserAgentWithOrchestrationId('my-app')).toBe('my-app') }) + + it('does not duplicate orchestration ID if already present in base', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'abc-123' + const alreadyTagged = 'my-app actions_orchestration_id/abc-123' + expect(getUserAgentWithOrchestrationId(alreadyTagged)).toBe( + alreadyTagged + ) + }) }) describe('public re-export', () => { @@ -110,5 +118,15 @@ describe('orchestration ID support', () => { const opts = getOctokitOptions('fake-token') expect(opts.userAgent).toBe('actions_orchestration_id/bad_chars_here_') }) + + it('does not duplicate orchestration ID when caller already applied it', () => { + process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id' + const opts = getOctokitOptions('fake-token', { + userAgent: 'my-app actions_orchestration_id/test-orch-id' + }) + expect(opts.userAgent).toBe( + 'my-app actions_orchestration_id/test-orch-id' + ) + }) }) }) diff --git a/packages/github/src/internal/utils.ts b/packages/github/src/internal/utils.ts index 5ff4db52fe..03c5822a6a 100644 --- a/packages/github/src/internal/utils.ts +++ b/packages/github/src/internal/utils.ts @@ -49,8 +49,10 @@ export function getUserAgentWithOrchestrationId( const orchId = process.env['ACTIONS_ORCHESTRATION_ID']?.trim() if (orchId) { const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_') + const tag = `actions_orchestration_id/${sanitizedId}` + if (baseUserAgent?.includes(tag)) return baseUserAgent const ua = baseUserAgent ? `${baseUserAgent} ` : '' - return `${ua}actions_orchestration_id/${sanitizedId}` + return `${ua}${tag}` } return baseUserAgent } From 3643ce2db4a129e7bac3dea3a35b3582447c8ec0 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Wed, 8 Apr 2026 19:38:31 +0000 Subject: [PATCH 4/4] style: fix prettier formatting in orchestration tests --- packages/github/__tests__/orchestration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/github/__tests__/orchestration.test.ts b/packages/github/__tests__/orchestration.test.ts index 0bda82f0a0..044fd45836 100644 --- a/packages/github/__tests__/orchestration.test.ts +++ b/packages/github/__tests__/orchestration.test.ts @@ -63,9 +63,7 @@ describe('orchestration ID support', () => { it('does not duplicate orchestration ID if already present in base', () => { process.env['ACTIONS_ORCHESTRATION_ID'] = 'abc-123' const alreadyTagged = 'my-app actions_orchestration_id/abc-123' - expect(getUserAgentWithOrchestrationId(alreadyTagged)).toBe( - alreadyTagged - ) + expect(getUserAgentWithOrchestrationId(alreadyTagged)).toBe(alreadyTagged) }) })