diff --git a/containers/api-proxy/Dockerfile b/containers/api-proxy/Dockerfile index 8dde31567..23f8f6bb0 100644 --- a/containers/api-proxy/Dockerfile +++ b/containers/api-proxy/Dockerfile @@ -52,7 +52,7 @@ COPY server.js logging.js metrics.js rate-limiter.js rate-limiter-window.js \ websocket-guards.js websocket-tunnel.js \ deprecated-header-tracker.js billing-headers.js upstream-response.js \ upstream-log.js upstream-retry.js upstream-token.js \ - anthropic-cache.js otel.js otel-exporters.js otel-serialization.js \ + anthropic-cache.js otel.js otel-exporters.js otel-serialization.js otel-workload-identity.js \ token-budget-log.js blocked-request-diagnostics.js \ provider-env-constants.js provider-env-constants.json provider-names.js \ model-api-mapping.js model-api-mapping.json ./ diff --git a/containers/api-proxy/otel-exporters.js b/containers/api-proxy/otel-exporters.js index 12bed86ec..28b19084c 100644 --- a/containers/api-proxy/otel-exporters.js +++ b/containers/api-proxy/otel-exporters.js @@ -27,8 +27,9 @@ class ProxyAwareOtlpExporter { * @param {Record} opts.headers - Extra request headers (auth etc.) * @param {string|null} opts.httpsProxy - Squid proxy URL, or falsy to connect directly * @param {import('@opentelemetry/resources').Resource} opts.resource + * @param {{getHeaders: () => Promise>}|null} [opts.headerProvider] */ - constructor({ url, headers, httpsProxy, resource }) { + constructor({ url, headers, httpsProxy, resource, headerProvider = null }) { const parsed = new URL(url); const trimmedPath = parsed.pathname.replace(/\/+$/, ''); if (trimmedPath === '' || trimmedPath === '/') { @@ -42,6 +43,7 @@ class ProxyAwareOtlpExporter { this._headers = headers || {}; this._agent = httpsProxy ? new HttpsProxyAgent(httpsProxy) : undefined; this._resource = resource; + this._headerProvider = headerProvider; } /** @@ -73,39 +75,44 @@ class ProxyAwareOtlpExporter { ? parseInt(this._parsedUrl.port, 10) : (isHttps ? 443 : 80); - const reqOptions = { - hostname: this._parsedUrl.hostname, - port, - path: `${this._parsedUrl.pathname}${this._parsedUrl.search || ''}`, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': bodyBuf.length, - ...this._headers, - }, - }; - if (this._agent) reqOptions.agent = this._agent; - - let req; - try { - req = Transport.request(reqOptions, (res) => { - res.on('data', () => {}); - res.on('error', (err) => { settle({ code: 1, error: err }); }); - res.on('end', () => { - const ok = res.statusCode >= 200 && res.statusCode < 300; - settle({ code: ok ? 0 : 1 }); + Promise.resolve(this._headerProvider ? this._headerProvider.getHeaders() : {}) + .then((dynamicHeaders) => { + const reqOptions = { + hostname: this._parsedUrl.hostname, + port, + path: `${this._parsedUrl.pathname}${this._parsedUrl.search || ''}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': bodyBuf.length, + ...this._headers, + ...dynamicHeaders, + }, + }; + if (this._agent) reqOptions.agent = this._agent; + + let req; + try { + req = Transport.request(reqOptions, (res) => { + res.on('data', () => {}); + res.on('error', (err) => { settle({ code: 1, error: err }); }); + res.on('end', () => { + const ok = res.statusCode >= 200 && res.statusCode < 300; + settle({ code: ok ? 0 : 1 }); + }); + }); + } catch (err) { + settle({ code: 1, error: err }); + return; + } + req.setTimeout(EXPORT_TIMEOUT_MS, () => { + req.destroy(new Error(`OTLP export timeout after ${EXPORT_TIMEOUT_MS}ms`)); }); - }); - } catch (err) { - settle({ code: 1, error: err }); - return; - } - req.setTimeout(EXPORT_TIMEOUT_MS, () => { - req.destroy(new Error(`OTLP export timeout after ${EXPORT_TIMEOUT_MS}ms`)); - }); - req.on('error', (err) => { settle({ code: 1, error: err }); }); - req.write(bodyBuf); - req.end(); + req.on('error', (err) => { settle({ code: 1, error: err }); }); + req.write(bodyBuf); + req.end(); + }) + .catch((err) => settle({ code: 1, error: err })); } shutdown() { return Promise.resolve(); } diff --git a/containers/api-proxy/otel-fanout.test.js b/containers/api-proxy/otel-fanout.test.js index 8d96016c2..9e1711ad7 100644 --- a/containers/api-proxy/otel-fanout.test.js +++ b/containers/api-proxy/otel-fanout.test.js @@ -7,6 +7,16 @@ const { FanOutSpanExporter } = require('./otel-exporters'); const { loadOtelModule } = require('./test-helpers/otel-test-utils'); +const mockWorkloadIdentity = { + matchesEndpoint: jest.fn(endpoint => endpoint === 'https://google.example.com:4318'), + getHeaders: jest.fn().mockResolvedValue({ Authorization: 'Bearer exchanged-token' }), + shutdown: jest.fn(), +}; + +jest.mock('./otel-workload-identity', () => ({ + createOtlpWorkloadIdentity: jest.fn(rawConfig => rawConfig ? mockWorkloadIdentity : null), +})); + // ── FanOutSpanExporter unit tests ───────────────────────────────────────────── describe('FanOutSpanExporter', () => { @@ -222,6 +232,40 @@ describe('otel fan-out initialization', () => { expect(otel.isEnabled()).toBe(true); }); + test('attaches workload identity only to its configured fan-out endpoint', () => { + const endpoints = [ + { url: 'https://google.example.com:4318' }, + { url: 'https://other.example.com:4318', headers: { Authorization: 'Other secret' } }, + ]; + const otel = loadOtelFresh({ + GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints), + GH_AW_OTLP_WORKLOAD_IDENTITY: JSON.stringify({ + provider: 'gcp', + audience: 'projects/123/providers/github', + endpoint: endpoints[0].url, + }), + }); + const processor = otel._provider.activeSpanProcessor._spanProcessors[0]; + const exporters = processor._exporter._exporters; + + expect(exporters[0]._headerProvider).toBe(mockWorkloadIdentity); + expect(exporters[1]._headerProvider).toBeNull(); + expect(exporters[1]._headers).toEqual({ Authorization: 'Other secret' }); + }); + + test('fails closed when workload identity matches no configured endpoint', () => { + expect(() => loadOtelFresh({ + GH_AW_OTLP_ENDPOINTS: JSON.stringify([ + { url: 'https://other.example.com:4318' }, + ]), + GH_AW_OTLP_WORKLOAD_IDENTITY: JSON.stringify({ + provider: 'gcp', + audience: 'projects/123/providers/github', + endpoint: 'https://google.example.com:4318', + }), + })).toThrow('does not match any configured OTLP endpoint'); + }); + test('uses FileSpanExporter when no OTLP config at all', () => { const otel = loadOtelFresh({}); expect(otel.isEnabled()).toBe(true); diff --git a/containers/api-proxy/otel-workload-identity.js b/containers/api-proxy/otel-workload-identity.js new file mode 100644 index 000000000..1d3118402 --- /dev/null +++ b/containers/api-proxy/otel-workload-identity.js @@ -0,0 +1,82 @@ +'use strict'; + +const { GcpOidcTokenProvider } = require('./gcp-oidc-token-provider'); + +function normalizeEndpoint(rawEndpoint) { + let endpoint; + try { + endpoint = new URL(rawEndpoint); + } catch { + throw new Error('OTLP workload identity requires a valid HTTPS endpoint'); + } + if (endpoint.protocol !== 'https:') { + throw new Error('OTLP workload identity requires a valid HTTPS endpoint'); + } + endpoint.hash = ''; + endpoint.pathname = endpoint.pathname.replace(/\/+$/, '') || '/'; + return endpoint.toString(); +} + +/** + * Creates an OTLP Authorization header provider from the workflow-generated + * workload identity configuration. The GitHub Actions runtime credentials stay + * inside the api-proxy sidecar; only the exchanged token reaches the collector. + * + * @param {string} rawConfig + * @returns {{getHeaders: () => Promise>, shutdown: () => void}|null} + */ +function createOtlpWorkloadIdentity(rawConfig) { + if (!rawConfig) return null; + + let config; + try { + config = JSON.parse(rawConfig); + } catch { + throw new Error('OTLP workload identity configuration must be valid JSON'); + } + + if (!config || typeof config !== 'object' + || !['gcp', 'google'].includes(config.provider) + || typeof config.audience !== 'string' || !config.audience.trim() + || typeof config.endpoint !== 'string' || !config.endpoint.trim()) { + throw new Error('OTLP workload identity requires provider "gcp", a non-empty audience, and an HTTPS endpoint'); + } + const endpoint = normalizeEndpoint(config.endpoint.trim()); + if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL || !process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN) { + throw new Error('OTLP workload identity requires GitHub Actions OIDC runtime credentials'); + } + + const provider = new GcpOidcTokenProvider({ + requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL, + requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN, + workloadIdentityProvider: config.audience.trim(), + oidcAudience: config.audience.trim(), + serviceAccount: typeof config['service-account'] === 'string' + ? config['service-account'].trim() || undefined + : undefined, + }); + const initialized = provider.initialize(); + + return { + matchesEndpoint(candidate) { + try { + return normalizeEndpoint(candidate) === endpoint; + } catch { + return false; + } + }, + async getHeaders() { + await initialized; + const token = provider.getToken(); + if (!token) { + throw new Error('OTLP workload identity token is unavailable'); + } + return { Authorization: 'Bearer ' + token }; + }, + shutdown() { + provider.shutdown(); + }, + }; +} + +module.exports = { createOtlpWorkloadIdentity, normalizeEndpoint }; diff --git a/containers/api-proxy/otel-workload-identity.test.js b/containers/api-proxy/otel-workload-identity.test.js new file mode 100644 index 000000000..6fee297e5 --- /dev/null +++ b/containers/api-proxy/otel-workload-identity.test.js @@ -0,0 +1,96 @@ +'use strict'; + +const mockInitialize = jest.fn().mockResolvedValue(undefined); +const mockGetToken = jest.fn().mockReturnValue('exchanged-token'); +const mockShutdown = jest.fn(); +const mockGcpOidcTokenProvider = jest.fn().mockImplementation(() => ({ + initialize: mockInitialize, + getToken: mockGetToken, + shutdown: mockShutdown, +})); + +jest.mock('./gcp-oidc-token-provider', () => ({ GcpOidcTokenProvider: mockGcpOidcTokenProvider })); + +const { createOtlpWorkloadIdentity } = require('./otel-workload-identity'); + +describe('createOtlpWorkloadIdentity', () => { + const savedEnv = { + requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL, + requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockInitialize.mockResolvedValue(undefined); + mockGetToken.mockReturnValue('exchanged-token'); + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = 'https://oidc.example/token'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'runtime-token'; + }); + + afterAll(() => { + if (savedEnv.requestUrl === undefined) delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL; + else process.env.ACTIONS_ID_TOKEN_REQUEST_URL = savedEnv.requestUrl; + if (savedEnv.requestToken === undefined) delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; + else process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = savedEnv.requestToken; + }); + + it('uses GCP WIF and service account impersonation for OTLP authorization', async () => { + const identity = createOtlpWorkloadIdentity(JSON.stringify({ + provider: 'gcp', + audience: 'projects/123/locations/global/workloadIdentityPools/pool/providers/github', + 'service-account': 'telemetry@example.iam.gserviceaccount.com', + endpoint: 'https://telemetry.googleapis.com/', + })); + + expect(mockGcpOidcTokenProvider).toHaveBeenCalledWith({ + requestUrl: 'https://oidc.example/token', + requestToken: 'runtime-token', + workloadIdentityProvider: 'projects/123/locations/global/workloadIdentityPools/pool/providers/github', + oidcAudience: 'projects/123/locations/global/workloadIdentityPools/pool/providers/github', + serviceAccount: 'telemetry@example.iam.gserviceaccount.com', + }); + expect(await identity.getHeaders()).toEqual({ Authorization: 'Bearer ' + 'exchanged-token' }); + expect(identity.matchesEndpoint('https://telemetry.googleapis.com')).toBe(true); + expect(identity.matchesEndpoint('https://other.example.com')).toBe(false); + identity.shutdown(); + expect(mockShutdown).toHaveBeenCalled(); + }); + + it.each([ + 'not-json', + JSON.stringify({ provider: 'azure', audience: 'audience' }), + JSON.stringify({ provider: 'gcp' }), + JSON.stringify({ + provider: 'gcp', + audience: 'projects/123/providers/github', + endpoint: 'http://telemetry.googleapis.com', + }), + ])('fails closed for invalid workload identity config: %s', (config) => { + expect(() => createOtlpWorkloadIdentity(config)).toThrow('OTLP workload identity'); + expect(mockGcpOidcTokenProvider).not.toHaveBeenCalled(); + }); + + it('fails export authorization when the exchange does not yield a token', async () => { + mockGetToken.mockReturnValue(null); + const identity = createOtlpWorkloadIdentity(JSON.stringify({ + provider: 'google', + audience: 'projects/123/locations/global/workloadIdentityPools/pool/providers/github', + endpoint: 'https://telemetry.googleapis.com', + })); + + await expect(identity.getHeaders()).rejects.toThrow('OTLP workload identity token is unavailable'); + identity.shutdown(); + }); + + it('normalizes only insignificant trailing slashes when matching endpoints', () => { + const identity = createOtlpWorkloadIdentity(JSON.stringify({ + provider: 'gcp', + audience: 'projects/123/locations/global/workloadIdentityPools/pool/providers/github', + endpoint: 'https://collector.example.com/custom/', + })); + + expect(identity.matchesEndpoint('https://collector.example.com/custom')).toBe(true); + expect(identity.matchesEndpoint('https://collector.example.com/other')).toBe(false); + expect(identity.matchesEndpoint('not-a-url')).toBe(false); + }); +}); diff --git a/containers/api-proxy/otel.js b/containers/api-proxy/otel.js index 829f512e8..9ae3c756d 100644 --- a/containers/api-proxy/otel.js +++ b/containers/api-proxy/otel.js @@ -46,6 +46,7 @@ const { } = require('@opentelemetry/api'); const { parseOtlpHeaders, buildResourceSpans } = require('./otel-serialization'); const { ProxyAwareOtlpExporter, FileSpanExporter, FanOutSpanExporter } = require('./otel-exporters'); +const { createOtlpWorkloadIdentity } = require('./otel-workload-identity'); // ── Environment variables ───────────────────────────────────────────────────── const OTLP_ENDPOINTS_JSON = (process.env.GH_AW_OTLP_ENDPOINTS || '').trim(); @@ -55,6 +56,7 @@ const SERVICE_NAME = (process.env.OTEL_SERVICE_NAME || 'awf-ap const PARENT_TRACE_ID = (process.env.GITHUB_AW_OTEL_TRACE_ID || '').trim(); const PARENT_SPAN_ID = (process.env.GITHUB_AW_OTEL_PARENT_SPAN_ID || '').trim(); const HTTPS_PROXY_URL = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; +const OTLP_WORKLOAD_IDENTITY = (process.env.GH_AW_OTLP_WORKLOAD_IDENTITY || '').trim(); const SCOPE_NAME = 'awf-api-proxy'; const OTEL_LOG_FILE = '/var/log/api-proxy/otel.jsonl'; @@ -63,6 +65,7 @@ const OTEL_LOG_FILE = '/var/log/api-proxy/otel.jsonl'; let _provider = null; let _tracer = null; let _enabled = false; +let _workloadIdentity = null; // ── SDK initialisation ──────────────────────────────────────────────────────── @@ -104,9 +107,20 @@ function _parseEndpoints() { function _init() { const resource = new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }); + _workloadIdentity = createOtlpWorkloadIdentity(OTLP_WORKLOAD_IDENTITY); let exporter; const endpoints = _parseEndpoints(); + const configuredEndpointUrls = endpoints.length > 0 + ? endpoints.map(endpoint => endpoint.url) + : (OTLP_ENDPOINT ? [OTLP_ENDPOINT] : []); + if (_workloadIdentity + && !configuredEndpointUrls.some(endpoint => _workloadIdentity.matchesEndpoint(endpoint))) { + throw new Error('OTLP workload identity endpoint does not match any configured OTLP endpoint'); + } + const headerProviderFor = endpoint => ( + _workloadIdentity?.matchesEndpoint(endpoint) ? _workloadIdentity : null + ); if (endpoints.length > 1) { // Fan-out: send spans to all configured endpoints concurrently @@ -115,6 +129,7 @@ function _init() { headers: ep.headers || {}, httpsProxy: HTTPS_PROXY_URL || null, resource, + headerProvider: headerProviderFor(ep.url), })); exporter = new FanOutSpanExporter(exporters); } else if (endpoints.length === 1) { @@ -123,6 +138,7 @@ function _init() { headers: endpoints[0].headers || {}, httpsProxy: HTTPS_PROXY_URL || null, resource, + headerProvider: headerProviderFor(endpoints[0].url), }); } else if (OTLP_ENDPOINT) { // Legacy single-endpoint fallback @@ -131,6 +147,7 @@ function _init() { headers: parseOtlpHeaders(OTLP_HEADERS_RAW), httpsProxy: HTTPS_PROXY_URL || null, resource, + headerProvider: headerProviderFor(OTLP_ENDPOINT), }); } else { exporter = new FileSpanExporter(OTEL_LOG_FILE); @@ -338,6 +355,7 @@ async function shutdown() { try { await _provider.shutdown(); } catch { /* best-effort */ } + _workloadIdentity?.shutdown(); } /** @@ -361,4 +379,5 @@ module.exports = { _parseEndpoints, _parseOtlpHeaders: parseOtlpHeaders, _buildResourceSpans: buildResourceSpans, + _createOtlpWorkloadIdentity: createOtlpWorkloadIdentity, }; diff --git a/containers/api-proxy/test-helpers/otel-test-utils.js b/containers/api-proxy/test-helpers/otel-test-utils.js index 49d3c8c6b..3265780d0 100644 --- a/containers/api-proxy/test-helpers/otel-test-utils.js +++ b/containers/api-proxy/test-helpers/otel-test-utils.js @@ -7,6 +7,9 @@ const OTEL_ENV_KEYS = [ 'OTEL_SERVICE_NAME', 'GITHUB_AW_OTEL_TRACE_ID', 'GITHUB_AW_OTEL_PARENT_SPAN_ID', + 'GH_AW_OTLP_WORKLOAD_IDENTITY', + 'ACTIONS_ID_TOKEN_REQUEST_URL', + 'ACTIONS_ID_TOKEN_REQUEST_TOKEN', 'HTTPS_PROXY', 'HTTP_PROXY', 'AWF_VERSION', diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index a2b247883..379b73007 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -18,6 +18,24 @@ The API proxy sidecar is **always enabled**. It: - **Transparent proxying**: Agent code uses standard SDK environment variables - **Squid routing**: Outbound HTTP/HTTPS routes through Squid, with the trusted sidecar exempt from domain ACLs +### OTLP workload identity federation + +For Google Cloud OTLP collectors, gh-aw can supply +`GH_AW_OTLP_WORKLOAD_IDENTITY` as a JSON object with `provider: gcp` (or +`google`), the Workload Identity Provider resource in `audience`, and an +optional `service-account`. The required `endpoint` field binds the exchanged +credential to one exact HTTPS collector endpoint. In fan-out mode, other +collectors retain their endpoint-specific headers and never receive the Google +access token. AWF forwards the GitHub Actions OIDC runtime credentials only to +the api-proxy sidecar. The sidecar exchanges the JWT via `sts.googleapis.com`, +optionally impersonates the service account, and injects the short-lived bearer +token into OTLP export requests for that endpoint. The sidecar is trusted and +already exempt from the agent egress allowlist, so its STS calls are not blocked. + +This authenticates only spans exported by the api-proxy sidecar. It does not +configure cloud-token exchange for separate gh-aw jobs such as activation, +conclusion, or safe outputs; gh-aw must implement that per-job exchange. + :::note[Implementation vs. provider documentation] The `--enable-api-proxy` CLI flag is deprecated and ignored — it is kept only so existing command lines and workflows continue to work. `--no-enable-api-proxy` is rejected as a runtime error; the API proxy cannot be disabled. Do not add the deprecated flag to new commands. ::: diff --git a/src/services/api-proxy-env-config.test.ts b/src/services/api-proxy-env-config.test.ts index c36e06fb0..4e36e59d2 100644 --- a/src/services/api-proxy-env-config.test.ts +++ b/src/services/api-proxy-env-config.test.ts @@ -211,6 +211,9 @@ describe('buildOtelEnv', () => { 'OTEL_EXPORTER_OTLP_HEADERS', 'GITHUB_AW_OTEL_TRACE_ID', 'GITHUB_AW_OTEL_PARENT_SPAN_ID', + 'GH_AW_OTLP_WORKLOAD_IDENTITY', + 'ACTIONS_ID_TOKEN_REQUEST_URL', + 'ACTIONS_ID_TOKEN_REQUEST_TOKEN', 'OTEL_SERVICE_NAME', ]; @@ -252,6 +255,18 @@ describe('buildOtelEnv', () => { expect(env.GH_AW_OTLP_ENDPOINTS).toBe('[{"url":"https://otel.example.com"}]'); }); + it('forwards workload identity and GitHub OIDC runtime credentials together', () => { + process.env.GH_AW_OTLP_WORKLOAD_IDENTITY = '{"provider":"gcp","audience":"projects/123/providers/github"}'; + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = 'https://actions.example/oidc'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'runtime-token'; + + const env = buildOtelEnv(); + + expect(env.GH_AW_OTLP_WORKLOAD_IDENTITY).toBe(process.env.GH_AW_OTLP_WORKLOAD_IDENTITY); + expect(env.ACTIONS_ID_TOKEN_REQUEST_URL).toBe('https://actions.example/oidc'); + expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBe('runtime-token'); + }); + it('forwards GITHUB_AW_OTEL_TRACE_ID and GITHUB_AW_OTEL_PARENT_SPAN_ID when set', () => { process.env.GITHUB_AW_OTEL_TRACE_ID = 'trace-abc'; process.env.GITHUB_AW_OTEL_PARENT_SPAN_ID = 'span-xyz'; diff --git a/src/services/api-proxy-env-config.ts b/src/services/api-proxy-env-config.ts index b1ab54613..1a20f2fb5 100644 --- a/src/services/api-proxy-env-config.ts +++ b/src/services/api-proxy-env-config.ts @@ -163,6 +163,7 @@ function buildProxyRoutingEnv(networkConfig: NetworkConfig): Record { + const workloadIdentityConfigured = Boolean(process.env.GH_AW_OTLP_WORKLOAD_IDENTITY?.trim()); return { // GH_AW_OTLP_ENDPOINTS (JSON array) enables fan-out to multiple collectors. // OTEL_EXPORTER_OTLP_ENDPOINT is kept for backward compat (single-endpoint fallback). @@ -173,7 +174,12 @@ function buildOtelEnv(): Record { 'OTEL_EXPORTER_OTLP_HEADERS', 'GITHUB_AW_OTEL_TRACE_ID', 'GITHUB_AW_OTEL_PARENT_SPAN_ID', + 'GH_AW_OTLP_WORKLOAD_IDENTITY', ), + ...(workloadIdentityConfigured && pickEnvVars( + 'ACTIONS_ID_TOKEN_REQUEST_URL', + 'ACTIONS_ID_TOKEN_REQUEST_TOKEN', + )), OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME || 'awf-api-proxy', }; }