/** * issue-token GitHub Action script. * * Flow: * 1. Request a GitHub OIDC token via @actions/core.getIDToken() * 2. Exchange the OIDC token for a SentryAgent.ai access token via POST /oidc/token * 3. Set outputs: access-token (masked) and expires-at (ISO 8601) * * The access token is immediately registered with core.setSecret() so it never * appears in plaintext in workflow logs. * * Error handling: * - OIDC exchange failures emit a clear message with a link to the trust policy setup docs */ 'use strict'; const core = require('@actions/core'); const { HttpClient } = require('@actions/http-client'); /** * Exchanges a GitHub OIDC JWT for a SentryAgent.ai access token for a specific agent. * * @param {string} apiUrl - Base URL of the SentryAgent.ai AgentIdP API. * @param {string} oidcToken - GitHub OIDC JWT obtained from core.getIDToken(). * @param {string} agentId - UUID of the agent for which to issue a token. * @returns {Promise<{ accessToken: string; expiresIn: number }>} The access token and its TTL in seconds. * @throws {Error} If the exchange fails, with a message including trust policy setup instructions. */ async function exchangeOIDCToken(apiUrl, oidcToken, agentId) { const client = new HttpClient('sentryagent-issue-token/1.0'); const url = `${apiUrl}/api/v1/oidc/token`; const body = JSON.stringify({ provider: 'github', token: oidcToken, agentId, }); let response; try { response = await client.post(url, body, { 'Content-Type': 'application/json', Accept: 'application/json', }); } catch (err) { throw new Error( `Failed to reach the SentryAgent.ai OIDC token endpoint at ${url}. ` + `Check that the api-url input is correct and the API is reachable.\n` + `Underlying error: ${err instanceof Error ? err.message : String(err)}`, ); } const rawBody = await response.readBody(); const statusCode = response.message.statusCode ?? 0; if (statusCode === 403) { throw new Error( 'GitHub OIDC token exchange was rejected with HTTP 403 (Forbidden). ' + 'This usually means no trust policy has been registered for this repository.\n\n' + 'To fix this, register a trust policy by calling:\n' + ` POST ${apiUrl}/oidc/trust-policies\n` + ' Body: { "provider": "github", "repository": "org/repo", "agentId": "" }\n\n' + 'For full setup instructions, visit: https://docs.sentryagent.ai/github-actions#trust-policy', ); } if (statusCode < 200 || statusCode >= 300) { let detail = rawBody; try { const parsed = JSON.parse(rawBody); detail = parsed.message ?? parsed.error_description ?? rawBody; } catch { // use rawBody as-is } throw new Error( `OIDC token exchange failed with HTTP ${statusCode}: ${detail}\n` + 'For trust policy setup instructions, visit: https://docs.sentryagent.ai/github-actions#trust-policy', ); } let tokenData; try { tokenData = JSON.parse(rawBody); } catch { throw new Error(`OIDC token exchange returned non-JSON response: ${rawBody}`); } if (typeof tokenData.access_token !== 'string' || tokenData.access_token.length === 0) { throw new Error('OIDC token exchange response did not include an access_token.'); } const expiresIn = typeof tokenData.expires_in === 'number' ? tokenData.expires_in : 3600; return { accessToken: tokenData.access_token, expiresIn }; } /** * Computes an ISO 8601 expiry timestamp from a TTL in seconds. * * @param {number} expiresInSeconds - Number of seconds until the token expires. * @returns {string} ISO 8601 timestamp string. */ function computeExpiresAt(expiresInSeconds) { return new Date(Date.now() + expiresInSeconds * 1000).toISOString(); } /** * Main entry point for the issue-token GitHub Action. * * @returns {Promise} */ async function run() { try { // Read inputs const apiUrl = core.getInput('api-url', { required: true }).replace(/\/$/, ''); const agentId = core.getInput('agent-id', { required: true }); core.info(`Requesting GitHub OIDC token for audience: ${apiUrl}`); let oidcToken; try { oidcToken = await core.getIDToken(apiUrl); } catch (err) { throw new Error( 'Failed to obtain a GitHub OIDC token. ' + "Ensure the workflow has 'id-token: write' permission in its permissions block.\n\n" + 'Example:\n' + 'permissions:\n' + ' id-token: write\n' + ' contents: read\n\n' + `Underlying error: ${err instanceof Error ? err.message : String(err)}\n` + 'For setup instructions, visit: https://docs.sentryagent.ai/github-actions#trust-policy', ); } core.info(`Exchanging GitHub OIDC token for SentryAgent.ai access token (agent: ${agentId})...`); const { accessToken, expiresIn } = await exchangeOIDCToken(apiUrl, oidcToken, agentId); // Mask the token immediately — must happen before any logging or output core.setSecret(accessToken); const expiresAt = computeExpiresAt(expiresIn); core.setOutput('access-token', accessToken); core.setOutput('expires-at', expiresAt); core.info(`Access token issued successfully. Expires at: ${expiresAt}`); } catch (err) { core.setFailed(err instanceof Error ? err.message : String(err)); } } run();