Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2296060d04 | |||
| 458bfe18a0 | |||
| 4cfb9b5008 | |||
| 06542e382a | |||
| bcdf6ab5fb | |||
| 314f669f10 | |||
| a24275e21b | |||
| 872e620342 | |||
| 6d9c6fd2b1 | |||
| 008021df1c | |||
| d6bc0fdd64 | |||
| 8fd0328109 | |||
| a1f87ce118 |
+27
-13
@@ -16,7 +16,6 @@ export class ClaudeAgent implements Agent {
|
|||||||
startTime: 0,
|
startTime: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
if (!config.apiKey) {
|
if (!config.apiKey) {
|
||||||
throw new Error("Claude agent requires an API key");
|
throw new Error("Claude agent requires an API key");
|
||||||
@@ -50,7 +49,10 @@ export class ClaudeAgent implements Agent {
|
|||||||
try {
|
try {
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: "bash",
|
cmd: "bash",
|
||||||
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
args: [
|
||||||
|
"-c",
|
||||||
|
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93",
|
||||||
|
],
|
||||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
timeout: 120000, // 2 minute timeout
|
timeout: 120000, // 2 minute timeout
|
||||||
onStdout: () => {},
|
onStdout: () => {},
|
||||||
@@ -58,7 +60,9 @@ export class ClaudeAgent implements Agent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
|
throw new Error(
|
||||||
|
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info("Claude Code installed successfully");
|
core.info("Claude Code installed successfully");
|
||||||
@@ -74,7 +78,6 @@ export class ClaudeAgent implements Agent {
|
|||||||
core.info("Running Claude Code...");
|
core.info("Running Claude Code...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
||||||
console.log(boxString(prompt, { title: "Prompt" }));
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
const args = [
|
const args = [
|
||||||
@@ -82,12 +85,15 @@ export class ClaudeAgent implements Agent {
|
|||||||
"--output-format",
|
"--output-format",
|
||||||
"stream-json",
|
"stream-json",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
|
"--debug",
|
||||||
"--permission-mode",
|
"--permission-mode",
|
||||||
"bypassPermissions",
|
"bypassPermissions",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
|
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
|
||||||
throw new Error("GITHUB_INSTALLATION_TOKEN is required for GitHub integration");
|
throw new Error(
|
||||||
|
"GITHUB_INSTALLATION_TOKEN is required for GitHub integration"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
|
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
|
||||||
@@ -152,9 +158,9 @@ export class ClaudeAgent implements Agent {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
try {
|
try {
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
const errorMessage =
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
error instanceof Error ? error.message : "Unknown error";
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to execute Claude Code: ${errorMessage}`,
|
error: `Failed to execute Claude Code: ${errorMessage}`,
|
||||||
@@ -180,7 +186,12 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
["model", parsedChunk.model],
|
["model", parsedChunk.model],
|
||||||
["cwd", parsedChunk.cwd],
|
["cwd", parsedChunk.cwd],
|
||||||
["permission_mode", parsedChunk.permissionMode],
|
["permission_mode", parsedChunk.permissionMode],
|
||||||
["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
|
[
|
||||||
|
"tools",
|
||||||
|
parsedChunk.tools?.length
|
||||||
|
? `${parsedChunk.tools.length} tools`
|
||||||
|
: "none",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"mcp_servers",
|
"mcp_servers",
|
||||||
parsedChunk.mcp_servers?.length
|
parsedChunk.mcp_servers?.length
|
||||||
@@ -207,7 +218,9 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
for (const content of parsedChunk.message.content) {
|
for (const content of parsedChunk.message.content) {
|
||||||
if (content.type === "text") {
|
if (content.type === "text") {
|
||||||
if (content.text.trim()) {
|
if (content.text.trim()) {
|
||||||
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
core.info(
|
||||||
|
boxString(content.text.trim(), { title: "Claude Code" })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (content.type === "tool_use") {
|
} else if (content.type === "tool_use") {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
@@ -270,7 +283,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
core.info(` └─ bash_command: ${input.bash_command}`);
|
core.info(` └─ bash_command: ${input.bash_command}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,10 +303,12 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
|
|
||||||
case "result":
|
case "result":
|
||||||
if (parsedChunk.subtype === "success") {
|
if (parsedChunk.subtype === "success") {
|
||||||
|
|
||||||
core.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
[
|
||||||
|
"Cost",
|
||||||
|
`$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`,
|
||||||
|
],
|
||||||
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
||||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||||
|
|||||||
@@ -25506,6 +25506,12 @@ var core = __toESM(require_core(), 1);
|
|||||||
// mcp/config.ts
|
// mcp/config.ts
|
||||||
var actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
|
var actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
|
||||||
function createMcpConfig(githubInstallationToken) {
|
function createMcpConfig(githubInstallationToken) {
|
||||||
|
const githubRepository = process.env.GITHUB_REPOSITORY;
|
||||||
|
if (!githubRepository) {
|
||||||
|
throw new Error(
|
||||||
|
"GITHUB_REPOSITORY environment variable is required for MCP GitHub integration"
|
||||||
|
);
|
||||||
|
}
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
{
|
{
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
@@ -25513,7 +25519,9 @@ function createMcpConfig(githubInstallationToken) {
|
|||||||
command: "node",
|
command: "node",
|
||||||
args: [`${actionPath}/mcp/server.ts`],
|
args: [`${actionPath}/mcp/server.ts`],
|
||||||
env: {
|
env: {
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
|
GITHUB_REPOSITORY: githubRepository,
|
||||||
|
LOG_LEVEL: "debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25726,7 +25734,10 @@ var ClaudeAgent = class {
|
|||||||
try {
|
try {
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: "bash",
|
cmd: "bash",
|
||||||
args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
|
args: [
|
||||||
|
"-c",
|
||||||
|
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"
|
||||||
|
],
|
||||||
env: { ANTHROPIC_API_KEY: this.apiKey },
|
env: { ANTHROPIC_API_KEY: this.apiKey },
|
||||||
timeout: 12e4,
|
timeout: 12e4,
|
||||||
// 2 minute timeout
|
// 2 minute timeout
|
||||||
@@ -25735,7 +25746,9 @@ var ClaudeAgent = class {
|
|||||||
onStderr: (chunk) => process.stderr.write(chunk)
|
onStderr: (chunk) => process.stderr.write(chunk)
|
||||||
});
|
});
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
|
throw new Error(
|
||||||
|
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
core.info("Claude Code installed successfully");
|
core.info("Claude Code installed successfully");
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
@@ -25755,11 +25768,14 @@ var ClaudeAgent = class {
|
|||||||
"--output-format",
|
"--output-format",
|
||||||
"stream-json",
|
"stream-json",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
|
"--debug",
|
||||||
"--permission-mode",
|
"--permission-mode",
|
||||||
"bypassPermissions"
|
"bypassPermissions"
|
||||||
];
|
];
|
||||||
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
|
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
|
||||||
throw new Error("GITHUB_INSTALLATION_TOKEN is required for GitHub integration");
|
throw new Error(
|
||||||
|
"GITHUB_INSTALLATION_TOKEN is required for GitHub integration"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
|
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
|
||||||
console.log("\u{1F4CB} MCP Config:", mcpConfig);
|
console.log("\u{1F4CB} MCP Config:", mcpConfig);
|
||||||
@@ -25842,7 +25858,10 @@ function processJSONChunk(chunk, agent) {
|
|||||||
["model", parsedChunk.model],
|
["model", parsedChunk.model],
|
||||||
["cwd", parsedChunk.cwd],
|
["cwd", parsedChunk.cwd],
|
||||||
["permission_mode", parsedChunk.permissionMode],
|
["permission_mode", parsedChunk.permissionMode],
|
||||||
["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
|
[
|
||||||
|
"tools",
|
||||||
|
parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"mcp_servers",
|
"mcp_servers",
|
||||||
parsedChunk.mcp_servers?.length ? `${parsedChunk.mcp_servers.length} servers` : "none"
|
parsedChunk.mcp_servers?.length ? `${parsedChunk.mcp_servers.length} servers` : "none"
|
||||||
@@ -25863,7 +25882,9 @@ function processJSONChunk(chunk, agent) {
|
|||||||
for (const content of parsedChunk.message.content) {
|
for (const content of parsedChunk.message.content) {
|
||||||
if (content.type === "text") {
|
if (content.type === "text") {
|
||||||
if (content.text.trim()) {
|
if (content.text.trim()) {
|
||||||
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
core.info(
|
||||||
|
boxString(content.text.trim(), { title: "Claude Code" })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (content.type === "tool_use") {
|
} else if (content.type === "tool_use") {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
@@ -25930,7 +25951,10 @@ function processJSONChunk(chunk, agent) {
|
|||||||
if (parsedChunk.subtype === "success") {
|
if (parsedChunk.subtype === "success") {
|
||||||
core.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
[
|
||||||
|
"Cost",
|
||||||
|
`$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`
|
||||||
|
],
|
||||||
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
||||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
||||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
["Duration", `${parsedChunk.duration_ms}ms`],
|
||||||
@@ -25984,45 +26008,167 @@ async function main(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// utils/github.ts
|
// utils/github.ts
|
||||||
|
var import_node_crypto = require("node:crypto");
|
||||||
var core3 = __toESM(require_core(), 1);
|
var core3 = __toESM(require_core(), 1);
|
||||||
async function setupGitHubInstallationToken() {
|
|
||||||
|
// utils/repo-context.ts
|
||||||
|
function resolveRepoContext() {
|
||||||
|
const githubRepo = process.env.GITHUB_REPOSITORY;
|
||||||
|
if (!githubRepo) {
|
||||||
|
throw new Error("GITHUB_REPOSITORY environment variable is required");
|
||||||
|
}
|
||||||
|
const [owner, name] = githubRepo.split("/");
|
||||||
|
if (!owner || !name) {
|
||||||
|
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
|
||||||
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
// utils/github.ts
|
||||||
|
function checkExistingToken() {
|
||||||
const inputToken = core3.getInput("github_installation_token");
|
const inputToken = core3.getInput("github_installation_token");
|
||||||
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
const existingToken = inputToken || envToken;
|
return inputToken || envToken || null;
|
||||||
|
}
|
||||||
|
function isGitHubActionsEnvironment() {
|
||||||
|
return Boolean(process.env.GITHUB_ACTIONS);
|
||||||
|
}
|
||||||
|
async function acquireTokenViaOIDC() {
|
||||||
|
core3.info("Generating OIDC token...");
|
||||||
|
const oidcToken = await core3.getIDToken("pullfrog-api");
|
||||||
|
core3.info("OIDC token generated successfully");
|
||||||
|
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||||
|
core3.info("Exchanging OIDC token for installation token...");
|
||||||
|
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!tokenResponse.ok) {
|
||||||
|
const errorText = await tokenResponse.text();
|
||||||
|
throw new Error(
|
||||||
|
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const tokenData = await tokenResponse.json();
|
||||||
|
core3.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||||
|
return tokenData.token;
|
||||||
|
}
|
||||||
|
var base64UrlEncode = (str) => {
|
||||||
|
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
};
|
||||||
|
var generateJWT = (appId, privateKey) => {
|
||||||
|
const now = Math.floor(Date.now() / 1e3);
|
||||||
|
const payload = {
|
||||||
|
iat: now - 60,
|
||||||
|
exp: now + 5 * 60,
|
||||||
|
iss: appId
|
||||||
|
};
|
||||||
|
const header = {
|
||||||
|
alg: "RS256",
|
||||||
|
typ: "JWT"
|
||||||
|
};
|
||||||
|
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
||||||
|
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
||||||
|
const signaturePart = `${encodedHeader}.${encodedPayload}`;
|
||||||
|
const signature = (0, import_node_crypto.createSign)("RSA-SHA256").update(signaturePart).sign(privateKey, "base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
return `${signaturePart}.${signature}`;
|
||||||
|
};
|
||||||
|
var githubRequest = async (path, options = {}) => {
|
||||||
|
const { method = "GET", headers = {}, body } = options;
|
||||||
|
const url = `https://api.github.com${path}`;
|
||||||
|
const requestHeaders = {
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
|
||||||
|
...headers
|
||||||
|
};
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: requestHeaders,
|
||||||
|
...body && { body }
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API request failed: ${response.status} ${response.statusText}
|
||||||
|
${errorText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
var checkRepositoryAccess = async (token, repoOwner, repoName) => {
|
||||||
|
try {
|
||||||
|
const response = await githubRequest("/installation/repositories", {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return response.repositories.some(
|
||||||
|
(repo) => repo.owner.login === repoOwner && repo.name === repoName
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var createInstallationToken = async (jwt, installationId) => {
|
||||||
|
const response = await githubRequest(
|
||||||
|
`/app/installations/${installationId}/access_tokens`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.token;
|
||||||
|
};
|
||||||
|
var findInstallationId = async (jwt, repoOwner, repoName) => {
|
||||||
|
const installations = await githubRequest("/app/installations", {
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` }
|
||||||
|
});
|
||||||
|
for (const installation of installations) {
|
||||||
|
try {
|
||||||
|
const tempToken = await createInstallationToken(jwt, installation.id);
|
||||||
|
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
|
||||||
|
if (hasAccess) {
|
||||||
|
return installation.id;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`No installation found with access to ${repoOwner}/${repoName}. Ensure the GitHub App is installed on the target repository.`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
async function acquireTokenViaGitHubApp() {
|
||||||
|
const repoContext = resolveRepoContext();
|
||||||
|
const config = {
|
||||||
|
appId: process.env.GITHUB_APP_ID,
|
||||||
|
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
||||||
|
repoOwner: repoContext.owner,
|
||||||
|
repoName: repoContext.name
|
||||||
|
};
|
||||||
|
const jwt = generateJWT(config.appId, config.privateKey);
|
||||||
|
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
|
||||||
|
const token = await createInstallationToken(jwt, installationId);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
async function acquireNewToken() {
|
||||||
|
if (isGitHubActionsEnvironment()) {
|
||||||
|
return await acquireTokenViaOIDC();
|
||||||
|
} else {
|
||||||
|
return await acquireTokenViaGitHubApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function setupGitHubInstallationToken() {
|
||||||
|
const existingToken = checkExistingToken();
|
||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
core3.setSecret(existingToken);
|
core3.setSecret(existingToken);
|
||||||
core3.info("Using provided GitHub installation token");
|
core3.info("Using provided GitHub installation token");
|
||||||
return existingToken;
|
return existingToken;
|
||||||
}
|
}
|
||||||
core3.info("Generating OIDC token...");
|
const token = await acquireNewToken();
|
||||||
try {
|
core3.setSecret(token);
|
||||||
const oidcToken = await core3.getIDToken("pullfrog-api");
|
process.env.GITHUB_INSTALLATION_TOKEN = token;
|
||||||
core3.info("OIDC token generated successfully");
|
return token;
|
||||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
|
||||||
core3.info("Exchanging OIDC token for installation token...");
|
|
||||||
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!tokenResponse.ok) {
|
|
||||||
const errorText = await tokenResponse.text();
|
|
||||||
throw new Error(
|
|
||||||
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
core3.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
|
||||||
core3.setSecret(tokenData.token);
|
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
|
||||||
return tokenData.token;
|
|
||||||
} catch (error2) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to setup GitHub installation token: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// entry.ts
|
// entry.ts
|
||||||
|
|||||||
@@ -3,7 +3,21 @@
|
|||||||
*/
|
*/
|
||||||
const actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
|
const actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
|
||||||
|
|
||||||
|
// import { dirname } from "node:path";
|
||||||
|
// import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
// const __filename = fileURLToPath(import.meta.url);
|
||||||
|
// const __dirname = dirname(__filename);
|
||||||
|
// const actionPath = dirname(__dirname);
|
||||||
|
|
||||||
export function createMcpConfig(githubInstallationToken: string) {
|
export function createMcpConfig(githubInstallationToken: string) {
|
||||||
|
const githubRepository = process.env.GITHUB_REPOSITORY;
|
||||||
|
if (!githubRepository) {
|
||||||
|
throw new Error(
|
||||||
|
"GITHUB_REPOSITORY environment variable is required for MCP GitHub integration"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
{
|
{
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
@@ -12,6 +26,8 @@ export function createMcpConfig(githubInstallationToken: string) {
|
|||||||
args: [`${actionPath}/mcp/server.ts`],
|
args: [`${actionPath}/mcp/server.ts`],
|
||||||
env: {
|
env: {
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
|
GITHUB_REPOSITORY: githubRepository,
|
||||||
|
LOG_LEVEL: "debug",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-3
@@ -31,7 +31,9 @@ server.tool(
|
|||||||
|
|
||||||
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
if (!githubInstallationToken) {
|
if (!githubInstallationToken) {
|
||||||
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
|
throw new Error(
|
||||||
|
"GITHUB_INSTALLATION_TOKEN environment variable is required"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve repository context from environment
|
// Resolve repository context from environment
|
||||||
@@ -66,7 +68,8 @@ server.tool(
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -89,4 +92,4 @@ async function runServer() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
runServer().catch(console.error);
|
await runServer();
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.14",
|
"version": "0.0.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import arg from "arg";
|
|||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import { main } from "./main.ts";
|
import { main } from "./main.ts";
|
||||||
import { runAct } from "./utils/act.ts";
|
import { runAct } from "./utils/act.ts";
|
||||||
import { generateInstallationToken } from "./utils/generate-installation-token.ts";
|
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
||||||
import { setupTestRepo } from "./utils/setup.ts";
|
import { setupTestRepo } from "./utils/setup.ts";
|
||||||
|
|
||||||
config();
|
config();
|
||||||
@@ -53,10 +53,10 @@ export async function run(
|
|||||||
inputs.github_token = process.env.GITHUB_TOKEN;
|
inputs.github_token = process.env.GITHUB_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔑 Generating GitHub installation token...");
|
console.log("🔑 Setting up GitHub installation token...");
|
||||||
const installationToken = await generateInstallationToken();
|
const installationToken = await setupGitHubInstallationToken();
|
||||||
inputs.github_installation_token = installationToken;
|
inputs.github_installation_token = installationToken;
|
||||||
console.log("✅ GitHub installation token generated successfully");
|
console.log("✅ GitHub installation token setup successfully");
|
||||||
|
|
||||||
const envWithToken = {
|
const envWithToken = {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import { createSign } from "node:crypto";
|
|
||||||
import { config } from "dotenv";
|
|
||||||
import { resolveRepoContext } from "./repo-context.ts";
|
|
||||||
|
|
||||||
config();
|
|
||||||
|
|
||||||
interface GitHubAppConfig {
|
|
||||||
appId: string;
|
|
||||||
privateKey: string;
|
|
||||||
repoOwner: string;
|
|
||||||
repoName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Installation {
|
|
||||||
id: number;
|
|
||||||
account: {
|
|
||||||
login: string;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Repository {
|
|
||||||
owner: {
|
|
||||||
login: string;
|
|
||||||
};
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InstallationTokenResponse {
|
|
||||||
token: string;
|
|
||||||
expires_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RepositoriesResponse {
|
|
||||||
repositories: Repository[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const base64UrlEncode = (str: string): string => {
|
|
||||||
return Buffer.from(str)
|
|
||||||
.toString("base64")
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=/g, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateJWT = (appId: string, privateKey: string): string => {
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const payload = {
|
|
||||||
iat: now - 60,
|
|
||||||
exp: now + 5 * 60,
|
|
||||||
iss: appId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const header = {
|
|
||||||
alg: "RS256",
|
|
||||||
typ: "JWT",
|
|
||||||
};
|
|
||||||
|
|
||||||
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
||||||
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
||||||
const signaturePart = `${encodedHeader}.${encodedPayload}`;
|
|
||||||
|
|
||||||
const signature = createSign("RSA-SHA256")
|
|
||||||
.update(signaturePart)
|
|
||||||
.sign(privateKey, "base64")
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=/g, "");
|
|
||||||
|
|
||||||
return `${signaturePart}.${signature}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const githubRequest = async <T>(
|
|
||||||
path: string,
|
|
||||||
options: {
|
|
||||||
method?: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
body?: string;
|
|
||||||
} = {}
|
|
||||||
): Promise<T> => {
|
|
||||||
const { method = "GET", headers = {}, body } = options;
|
|
||||||
|
|
||||||
const url = `https://api.github.com${path}`;
|
|
||||||
const requestHeaders = {
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
|
|
||||||
...headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: requestHeaders,
|
|
||||||
...(body && { body }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`GitHub API request failed: ${response.status} ${response.statusText}\n${errorText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json() as T;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkRepositoryAccess = async (
|
|
||||||
token: string,
|
|
||||||
repoOwner: string,
|
|
||||||
repoName: string
|
|
||||||
): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const response = await githubRequest<RepositoriesResponse>("/installation/repositories", {
|
|
||||||
headers: { Authorization: `token ${token}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.repositories.some(
|
|
||||||
(repo) => repo.owner.login === repoOwner && repo.name === repoName
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createInstallationToken = async (jwt: string, installationId: number): Promise<string> => {
|
|
||||||
const response = await githubRequest<InstallationTokenResponse>(
|
|
||||||
`/app/installations/${installationId}/access_tokens`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { Authorization: `Bearer ${jwt}` },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.token;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findInstallationId = async (
|
|
||||||
jwt: string,
|
|
||||||
repoOwner: string,
|
|
||||||
repoName: string
|
|
||||||
): Promise<number> => {
|
|
||||||
const installations = await githubRequest<Installation[]>("/app/installations", {
|
|
||||||
headers: { Authorization: `Bearer ${jwt}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const installation of installations) {
|
|
||||||
try {
|
|
||||||
const tempToken = await createInstallationToken(jwt, installation.id);
|
|
||||||
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
|
|
||||||
|
|
||||||
if (hasAccess) {
|
|
||||||
return installation.id;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`No installation found with access to ${repoOwner}/${repoName}. ` +
|
|
||||||
"Ensure the GitHub App is installed on the target repository."
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateInstallationToken = async (): Promise<string> => {
|
|
||||||
const repoContext = resolveRepoContext();
|
|
||||||
|
|
||||||
const config: GitHubAppConfig = {
|
|
||||||
appId: process.env.GITHUB_APP_ID!,
|
|
||||||
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n")!,
|
|
||||||
repoOwner: repoContext.owner,
|
|
||||||
repoName: repoContext.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
const jwt = generateJWT(config.appId, config.privateKey);
|
|
||||||
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
|
|
||||||
const token = await createInstallationToken(jwt, installationId);
|
|
||||||
|
|
||||||
return token;
|
|
||||||
};
|
|
||||||
+229
-41
@@ -1,4 +1,6 @@
|
|||||||
|
import { createSign } from "node:crypto";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import { resolveRepoContext } from "./repo-context.ts";
|
||||||
|
|
||||||
export interface InstallationToken {
|
export interface InstallationToken {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -10,55 +12,241 @@ export interface InstallationToken {
|
|||||||
owner?: string;
|
owner?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GitHubAppConfig {
|
||||||
|
appId: string;
|
||||||
|
privateKey: string;
|
||||||
|
repoOwner: string;
|
||||||
|
repoName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Installation {
|
||||||
|
id: number;
|
||||||
|
account: {
|
||||||
|
login: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Repository {
|
||||||
|
owner: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstallationTokenResponse {
|
||||||
|
token: string;
|
||||||
|
expires_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RepositoriesResponse {
|
||||||
|
repositories: Repository[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkExistingToken(): string | null {
|
||||||
|
const inputToken = core.getInput("github_installation_token");
|
||||||
|
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
||||||
|
return inputToken || envToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGitHubActionsEnvironment(): boolean {
|
||||||
|
return Boolean(process.env.GITHUB_ACTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireTokenViaOIDC(): Promise<string> {
|
||||||
|
core.info("Generating OIDC token...");
|
||||||
|
|
||||||
|
const oidcToken = await core.getIDToken("pullfrog-api");
|
||||||
|
core.info("OIDC token generated successfully");
|
||||||
|
|
||||||
|
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||||
|
|
||||||
|
core.info("Exchanging OIDC token for installation token...");
|
||||||
|
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenResponse.ok) {
|
||||||
|
const errorText = await tokenResponse.text();
|
||||||
|
throw new Error(
|
||||||
|
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
||||||
|
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||||
|
|
||||||
|
return tokenData.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64UrlEncode = (str: string): string => {
|
||||||
|
return Buffer.from(str)
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateJWT = (appId: string, privateKey: string): string => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const payload = {
|
||||||
|
iat: now - 60,
|
||||||
|
exp: now + 5 * 60,
|
||||||
|
iss: appId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = {
|
||||||
|
alg: "RS256",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
||||||
|
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
||||||
|
const signaturePart = `${encodedHeader}.${encodedPayload}`;
|
||||||
|
|
||||||
|
const signature = createSign("RSA-SHA256")
|
||||||
|
.update(signaturePart)
|
||||||
|
.sign(privateKey, "base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
|
||||||
|
return `${signaturePart}.${signature}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const githubRequest = async <T>(
|
||||||
|
path: string,
|
||||||
|
options: {
|
||||||
|
method?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: string;
|
||||||
|
} = {}
|
||||||
|
): Promise<T> => {
|
||||||
|
const { method = "GET", headers = {}, body } = options;
|
||||||
|
|
||||||
|
const url = `https://api.github.com${path}`;
|
||||||
|
const requestHeaders = {
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
|
||||||
|
...headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: requestHeaders,
|
||||||
|
...(body && { body }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API request failed: ${response.status} ${response.statusText}\n${errorText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkRepositoryAccess = async (
|
||||||
|
token: string,
|
||||||
|
repoOwner: string,
|
||||||
|
repoName: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await githubRequest<RepositoriesResponse>("/installation/repositories", {
|
||||||
|
headers: { Authorization: `token ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.repositories.some(
|
||||||
|
(repo) => repo.owner.login === repoOwner && repo.name === repoName
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInstallationToken = async (jwt: string, installationId: number): Promise<string> => {
|
||||||
|
const response = await githubRequest<InstallationTokenResponse>(
|
||||||
|
`/app/installations/${installationId}/access_tokens`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findInstallationId = async (
|
||||||
|
jwt: string,
|
||||||
|
repoOwner: string,
|
||||||
|
repoName: string
|
||||||
|
): Promise<number> => {
|
||||||
|
const installations = await githubRequest<Installation[]>("/app/installations", {
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const installation of installations) {
|
||||||
|
try {
|
||||||
|
const tempToken = await createInstallationToken(jwt, installation.id);
|
||||||
|
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
return installation.id;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`No installation found with access to ${repoOwner}/${repoName}. ` +
|
||||||
|
"Ensure the GitHub App is installed on the target repository."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function acquireTokenViaGitHubApp(): Promise<string> {
|
||||||
|
const repoContext = resolveRepoContext();
|
||||||
|
|
||||||
|
const config: GitHubAppConfig = {
|
||||||
|
appId: process.env.GITHUB_APP_ID!,
|
||||||
|
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n")!,
|
||||||
|
repoOwner: repoContext.owner,
|
||||||
|
repoName: repoContext.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwt = generateJWT(config.appId, config.privateKey);
|
||||||
|
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
|
||||||
|
const token = await createInstallationToken(jwt, installationId);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireNewToken(): Promise<string> {
|
||||||
|
if (isGitHubActionsEnvironment()) {
|
||||||
|
return await acquireTokenViaOIDC();
|
||||||
|
} else {
|
||||||
|
return await acquireTokenViaGitHubApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup GitHub installation token for the action
|
* Setup GitHub installation token for the action
|
||||||
*/
|
*/
|
||||||
export async function setupGitHubInstallationToken(): Promise<string> {
|
export async function setupGitHubInstallationToken(): Promise<string> {
|
||||||
const inputToken = core.getInput("github_installation_token");
|
const existingToken = checkExistingToken();
|
||||||
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
|
|
||||||
|
|
||||||
const existingToken = inputToken || envToken;
|
|
||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
core.setSecret(existingToken);
|
core.setSecret(existingToken);
|
||||||
core.info("Using provided GitHub installation token");
|
core.info("Using provided GitHub installation token");
|
||||||
return existingToken;
|
return existingToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info("Generating OIDC token...");
|
const token = await acquireNewToken();
|
||||||
|
|
||||||
try {
|
core.setSecret(token);
|
||||||
const oidcToken = await core.getIDToken("pullfrog-api");
|
process.env.GITHUB_INSTALLATION_TOKEN = token;
|
||||||
core.info("OIDC token generated successfully");
|
|
||||||
|
return token;
|
||||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
|
||||||
|
|
||||||
core.info("Exchanging OIDC token for installation token...");
|
|
||||||
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tokenResponse.ok) {
|
|
||||||
const errorText = await tokenResponse.text();
|
|
||||||
throw new Error(
|
|
||||||
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
|
||||||
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
|
||||||
|
|
||||||
core.setSecret(tokenData.token);
|
|
||||||
|
|
||||||
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
|
|
||||||
|
|
||||||
return tokenData.token;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to setup GitHub installation token: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user