Compare commits

..

7 Commits

Author SHA1 Message Date
Colin McDonnell 5c8b03a427 Lock 2025-09-10 00:56:50 -07:00
Colin McDonnell 92225d30c5 No frozen lockfile 2025-09-10 00:36:03 -07:00
Colin McDonnell 3630ba6618 0.0.8 2025-09-10 00:32:54 -07:00
Colin McDonnell 6a03bb8e1b Update precommit 2025-09-10 00:31:21 -07:00
Colin McDonnell 3139f541e4 feat: integrate OIDC token exchange in GitHub Action
- Add setupGitHubInstallationToken utility for OIDC token generation
- Implement automatic token exchange with Pullfrog API endpoint
- Add support for multiple authentication methods (input, env, OIDC)
- Create setup utilities for test repository management
- Update action entry point to handle new token flow
- Add environment variable documentation for API key
- Remove large bundled dependencies and optimize build
- Support both development and production token workflows
2025-09-10 00:30:45 -07:00
Colin McDonnell c5b9c7cfc4 Tweak 2025-09-09 17:19:59 -07:00
Colin McDonnell 7a14716481 Disable npm publishing for now 2025-09-09 16:58:30 -07:00
18 changed files with 422 additions and 26242 deletions
+7 -17
View File
@@ -34,7 +34,7 @@ jobs:
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install --no-frozen-lockfile
- name: Get package version - name: Get package version
id: version id: version
@@ -95,24 +95,14 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ steps.version.outputs.tag }} tag_name: ${{ steps.version.outputs.tag }}
release_name: "@pullfrog/action ${{ steps.version.outputs.tag }}" release_name: "${{ steps.version.outputs.tag }}"
body: | body: |
## 📦 @pullfrog/action ${{ steps.version.outputs.version }} ## 📦 @pullfrog/action ${{ steps.version.outputs.version }}
### Usage in GitHub Actions ### Usage in GitHub Actions
Use the major version tag for automatic updates:
```yaml ```yaml
- uses: pullfrog/action@${{ steps.version.outputs.major_tag }} - uses: pullfrog/action@${{ steps.version.outputs.major_tag }}
with:
message: "Your message here"
```
Or pin to this specific version:
```yaml
- uses: pullfrog/action@${{ steps.version.outputs.tag }}
with:
message: "Your message here"
``` ```
### Installation via npm ### Installation via npm
@@ -123,11 +113,11 @@ jobs:
draft: false draft: false
prerelease: false prerelease: false
- name: Publish to npm # - name: Publish to npm
if: steps.check_tag.outputs.exists == 'false' # if: steps.check_tag.outputs.exists == 'false'
run: npm publish --access public # run: npm publish --access public
env: # env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Summary - name: Summary
if: always() if: always()
+1 -1
View File
@@ -3,4 +3,4 @@ echo "🔨 Building action..."
npm run build npm run build
# Add the built files to the commit # Add the built files to the commit
git add index.cjs entry.cjs git add entry.cjs
+8
View File
@@ -55,6 +55,14 @@ pnpm dev # Watch mode
The action is bundled into `entry.cjs` with all dependencies included, eliminating runtime dependency on node_modules. The action is bundled into `entry.cjs` with all dependencies included, eliminating runtime dependency on node_modules.
## Environment Variables
Create `.env` in `/action`:
```bash
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Claude API key
```
## Architecture ## Architecture
- **entry.cjs**: Bundled action entry point (self-contained) - **entry.cjs**: Bundled action entry point (self-contained)
+6
View File
@@ -10,6 +10,12 @@ inputs:
anthropic_api_key: anthropic_api_key:
description: "Anthropic API key for Claude Code authentication" description: "Anthropic API key for Claude Code authentication"
required: false required: false
github_token:
description: "GitHub token for repository access"
required: false
github_installation_token:
description: "GitHub App installation token"
required: false
runs: runs:
using: "node20" using: "node20"
+4 -1
View File
@@ -59,7 +59,10 @@ export class ClaudeAgent implements Agent {
], ],
env: { ANTHROPIC_API_KEY: this.apiKey }, env: { ANTHROPIC_API_KEY: this.apiKey },
timeout: 120000, // 2 minute timeout timeout: 120000, // 2 minute timeout
onStdout: (chunk) => process.stdout.write(chunk), onStdout: (chunk) => {
// no logs
// process.stdout.write(chunk)
},
onStderr: (chunk) => process.stderr.write(chunk), onStderr: (chunk) => process.stderr.write(chunk),
}); });
+142 -92
View File
@@ -13047,7 +13047,7 @@ var require_fetch = __commonJS({
this.emit("terminated", error2); this.emit("terminated", error2);
} }
}; };
function fetch(input, init = {}) { function fetch2(input, init = {}) {
webidl.argumentLengthCheck(arguments, 1, { header: "globalThis.fetch" }); webidl.argumentLengthCheck(arguments, 1, { header: "globalThis.fetch" });
const p = createDeferredPromise(); const p = createDeferredPromise();
let requestObject; let requestObject;
@@ -13977,7 +13977,7 @@ var require_fetch = __commonJS({
} }
} }
module2.exports = { module2.exports = {
fetch, fetch: fetch2,
Fetch, Fetch,
fetching, fetching,
finalizeAndReportTiming finalizeAndReportTiming
@@ -17233,7 +17233,7 @@ var require_undici = __commonJS({
module2.exports.getGlobalDispatcher = getGlobalDispatcher; module2.exports.getGlobalDispatcher = getGlobalDispatcher;
if (util.nodeMajor > 16 || util.nodeMajor === 16 && util.nodeMinor >= 8) { if (util.nodeMajor > 16 || util.nodeMajor === 16 && util.nodeMinor >= 8) {
let fetchImpl = null; let fetchImpl = null;
module2.exports.fetch = async function fetch(resource) { module2.exports.fetch = async function fetch2(resource) {
if (!fetchImpl) { if (!fetchImpl) {
fetchImpl = require_fetch().fetch; fetchImpl = require_fetch().fetch;
} }
@@ -17582,12 +17582,12 @@ var require_lib = __commonJS({
throw new Error("Client has already been disposed."); throw new Error("Client has already been disposed.");
} }
const parsedUrl = new URL(requestUrl); const parsedUrl = new URL(requestUrl);
let info3 = this._prepareRequest(verb, parsedUrl, headers); let info4 = this._prepareRequest(verb, parsedUrl, headers);
const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) ? this._maxRetries + 1 : 1; const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) ? this._maxRetries + 1 : 1;
let numTries = 0; let numTries = 0;
let response; let response;
do { do {
response = yield this.requestRaw(info3, data); response = yield this.requestRaw(info4, data);
if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
let authenticationHandler; let authenticationHandler;
for (const handler of this.handlers) { for (const handler of this.handlers) {
@@ -17597,7 +17597,7 @@ var require_lib = __commonJS({
} }
} }
if (authenticationHandler) { if (authenticationHandler) {
return authenticationHandler.handleAuthentication(this, info3, data); return authenticationHandler.handleAuthentication(this, info4, data);
} else { } else {
return response; return response;
} }
@@ -17620,8 +17620,8 @@ var require_lib = __commonJS({
} }
} }
} }
info3 = this._prepareRequest(verb, parsedRedirectUrl, headers); info4 = this._prepareRequest(verb, parsedRedirectUrl, headers);
response = yield this.requestRaw(info3, data); response = yield this.requestRaw(info4, data);
redirectsRemaining--; redirectsRemaining--;
} }
if (!response.message.statusCode || !HttpResponseRetryCodes.includes(response.message.statusCode)) { if (!response.message.statusCode || !HttpResponseRetryCodes.includes(response.message.statusCode)) {
@@ -17650,7 +17650,7 @@ var require_lib = __commonJS({
* @param info * @param info
* @param data * @param data
*/ */
requestRaw(info3, data) { requestRaw(info4, data) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
function callbackForResult(err, res) { function callbackForResult(err, res) {
@@ -17662,7 +17662,7 @@ var require_lib = __commonJS({
resolve(res); resolve(res);
} }
} }
this.requestRawWithCallback(info3, data, callbackForResult); this.requestRawWithCallback(info4, data, callbackForResult);
}); });
}); });
} }
@@ -17672,12 +17672,12 @@ var require_lib = __commonJS({
* @param data * @param data
* @param onResult * @param onResult
*/ */
requestRawWithCallback(info3, data, onResult) { requestRawWithCallback(info4, data, onResult) {
if (typeof data === "string") { if (typeof data === "string") {
if (!info3.options.headers) { if (!info4.options.headers) {
info3.options.headers = {}; info4.options.headers = {};
} }
info3.options.headers["Content-Length"] = Buffer.byteLength(data, "utf8"); info4.options.headers["Content-Length"] = Buffer.byteLength(data, "utf8");
} }
let callbackCalled = false; let callbackCalled = false;
function handleResult(err, res) { function handleResult(err, res) {
@@ -17686,7 +17686,7 @@ var require_lib = __commonJS({
onResult(err, res); onResult(err, res);
} }
} }
const req = info3.httpModule.request(info3.options, (msg) => { const req = info4.httpModule.request(info4.options, (msg) => {
const res = new HttpClientResponse(msg); const res = new HttpClientResponse(msg);
handleResult(void 0, res); handleResult(void 0, res);
}); });
@@ -17698,7 +17698,7 @@ var require_lib = __commonJS({
if (socket) { if (socket) {
socket.end(); socket.end();
} }
handleResult(new Error(`Request timeout: ${info3.options.path}`)); handleResult(new Error(`Request timeout: ${info4.options.path}`));
}); });
req.on("error", function(err) { req.on("error", function(err) {
handleResult(err); handleResult(err);
@@ -17734,27 +17734,27 @@ var require_lib = __commonJS({
return this._getProxyAgentDispatcher(parsedUrl, proxyUrl); return this._getProxyAgentDispatcher(parsedUrl, proxyUrl);
} }
_prepareRequest(method, requestUrl, headers) { _prepareRequest(method, requestUrl, headers) {
const info3 = {}; const info4 = {};
info3.parsedUrl = requestUrl; info4.parsedUrl = requestUrl;
const usingSsl = info3.parsedUrl.protocol === "https:"; const usingSsl = info4.parsedUrl.protocol === "https:";
info3.httpModule = usingSsl ? https : http; info4.httpModule = usingSsl ? https : http;
const defaultPort = usingSsl ? 443 : 80; const defaultPort = usingSsl ? 443 : 80;
info3.options = {}; info4.options = {};
info3.options.host = info3.parsedUrl.hostname; info4.options.host = info4.parsedUrl.hostname;
info3.options.port = info3.parsedUrl.port ? parseInt(info3.parsedUrl.port) : defaultPort; info4.options.port = info4.parsedUrl.port ? parseInt(info4.parsedUrl.port) : defaultPort;
info3.options.path = (info3.parsedUrl.pathname || "") + (info3.parsedUrl.search || ""); info4.options.path = (info4.parsedUrl.pathname || "") + (info4.parsedUrl.search || "");
info3.options.method = method; info4.options.method = method;
info3.options.headers = this._mergeHeaders(headers); info4.options.headers = this._mergeHeaders(headers);
if (this.userAgent != null) { if (this.userAgent != null) {
info3.options.headers["user-agent"] = this.userAgent; info4.options.headers["user-agent"] = this.userAgent;
} }
info3.options.agent = this._getAgent(info3.parsedUrl); info4.options.agent = this._getAgent(info4.parsedUrl);
if (this.handlers) { if (this.handlers) {
for (const handler of this.handlers) { for (const handler of this.handlers) {
handler.prepareRequest(info3.options); handler.prepareRequest(info4.options);
} }
} }
return info3; return info4;
} }
_mergeHeaders(headers) { _mergeHeaders(headers) {
if (this.requestOptions && this.requestOptions.headers) { if (this.requestOptions && this.requestOptions.headers) {
@@ -19675,7 +19675,7 @@ var require_core = __commonJS({
process.env["PATH"] = `${inputPath}${path.delimiter}${process.env["PATH"]}`; process.env["PATH"] = `${inputPath}${path.delimiter}${process.env["PATH"]}`;
} }
exports2.addPath = addPath; exports2.addPath = addPath;
function getInput2(name, options) { function getInput3(name, options) {
const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || ""; const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) { if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`); throw new Error(`Input required and not supplied: ${name}`);
@@ -19685,9 +19685,9 @@ var require_core = __commonJS({
} }
return val.trim(); return val.trim();
} }
exports2.getInput = getInput2; exports2.getInput = getInput3;
function getMultilineInput(name, options) { function getMultilineInput(name, options) {
const inputs = getInput2(name, options).split("\n").filter((x) => x !== ""); const inputs = getInput3(name, options).split("\n").filter((x) => x !== "");
if (options && options.trimWhitespace === false) { if (options && options.trimWhitespace === false) {
return inputs; return inputs;
} }
@@ -19697,7 +19697,7 @@ var require_core = __commonJS({
function getBooleanInput(name, options) { function getBooleanInput(name, options) {
const trueValue = ["true", "True", "TRUE"]; const trueValue = ["true", "True", "TRUE"];
const falseValue = ["false", "False", "FALSE"]; const falseValue = ["false", "False", "FALSE"];
const val = getInput2(name, options); const val = getInput3(name, options);
if (trueValue.includes(val)) if (trueValue.includes(val))
return true; return true;
if (falseValue.includes(val)) if (falseValue.includes(val))
@@ -19706,7 +19706,7 @@ var require_core = __commonJS({
Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
} }
exports2.getBooleanInput = getBooleanInput; exports2.getBooleanInput = getBooleanInput;
function setOutput2(name, value) { function setOutput(name, value) {
const filePath = process.env["GITHUB_OUTPUT"] || ""; const filePath = process.env["GITHUB_OUTPUT"] || "";
if (filePath) { if (filePath) {
return (0, file_command_1.issueFileCommand)("OUTPUT", (0, file_command_1.prepareKeyValueMessage)(name, value)); return (0, file_command_1.issueFileCommand)("OUTPUT", (0, file_command_1.prepareKeyValueMessage)(name, value));
@@ -19714,7 +19714,7 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
process.stdout.write(os.EOL); process.stdout.write(os.EOL);
(0, command_1.issueCommand)("set-output", { name }, (0, utils_1.toCommandValue)(value)); (0, command_1.issueCommand)("set-output", { name }, (0, utils_1.toCommandValue)(value));
} }
exports2.setOutput = setOutput2; exports2.setOutput = setOutput;
function setCommandEcho(enabled) { function setCommandEcho(enabled) {
(0, command_1.issue)("echo", enabled ? "on" : "off"); (0, command_1.issue)("echo", enabled ? "on" : "off");
} }
@@ -19744,10 +19744,10 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
(0, command_1.issueCommand)("notice", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); (0, command_1.issueCommand)("notice", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message);
} }
exports2.notice = notice; exports2.notice = notice;
function info3(message) { function info4(message) {
process.stdout.write(message + os.EOL); process.stdout.write(message + os.EOL);
} }
exports2.info = info3; exports2.info = info4;
function startGroup2(name) { function startGroup2(name) {
(0, command_1.issue)("group", name); (0, command_1.issue)("group", name);
} }
@@ -19781,12 +19781,12 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
return process.env[`STATE_${name}`] || ""; return process.env[`STATE_${name}`] || "";
} }
exports2.getState = getState; exports2.getState = getState;
function getIDToken(aud) { function getIDToken2(aud) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
return yield oidc_utils_1.OidcClient.getIDToken(aud); return yield oidc_utils_1.OidcClient.getIDToken(aud);
}); });
} }
exports2.getIDToken = getIDToken; exports2.getIDToken = getIDToken2;
var summary_1 = require_summary(); var summary_1 = require_summary();
Object.defineProperty(exports2, "summary", { enumerable: true, get: function() { Object.defineProperty(exports2, "summary", { enumerable: true, get: function() {
return summary_1.summary; return summary_1.summary;
@@ -25494,14 +25494,55 @@ var require_src = __commonJS({
}); });
// entry.ts // entry.ts
var core3 = __toESM(require_core(), 1); var core4 = __toESM(require_core(), 1);
// main.ts // main.ts
var core2 = __toESM(require_core(), 1); var core3 = __toESM(require_core(), 1);
// agents/claude.ts // agents/claude.ts
var import_promises = require("node:fs/promises"); var import_promises = require("node:fs/promises");
var core2 = __toESM(require_core(), 1);
// utils/github.ts
var core = __toESM(require_core(), 1); var core = __toESM(require_core(), 1);
async function setupGitHubInstallationToken() {
const inputToken = core.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
const existingToken = inputToken || envToken;
if (existingToken) {
core.info("Using provided GitHub installation token");
return existingToken;
}
core.info("No cached installation token found, generating OIDC token...");
try {
const oidcToken = await core.getIDToken("pullfrog-api");
core.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
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();
core.info(
`Installation token obtained for ${tokenData.owner}/${tokenData.repository || "all repositories"}`
);
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"}`
);
}
}
// utils/subprocess.ts // utils/subprocess.ts
var import_node_child_process = require("node:child_process"); var import_node_child_process = require("node:child_process");
@@ -25700,10 +25741,10 @@ var ClaudeAgent = class {
*/ */
async install() { async install() {
if (await this.isClaudeInstalled()) { if (await this.isClaudeInstalled()) {
core.info("Claude Code is already installed, skipping installation"); core2.info("Claude Code is already installed, skipping installation");
return; return;
} }
core.info("Installing Claude Code..."); core2.info("Installing Claude Code...");
try { try {
const result = await spawn({ const result = await spawn({
cmd: "bash", cmd: "bash",
@@ -25714,7 +25755,8 @@ var ClaudeAgent = class {
env: { ANTHROPIC_API_KEY: this.apiKey }, env: { ANTHROPIC_API_KEY: this.apiKey },
timeout: 12e4, timeout: 12e4,
// 2 minute timeout // 2 minute timeout
onStdout: (chunk) => process.stdout.write(chunk), onStdout: (chunk) => {
},
onStderr: (chunk) => process.stderr.write(chunk) onStderr: (chunk) => process.stderr.write(chunk)
}); });
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
@@ -25722,7 +25764,7 @@ var ClaudeAgent = class {
`Installation failed with exit code ${result.exitCode}: ${result.stderr}` `Installation failed with exit code ${result.exitCode}: ${result.stderr}`
); );
} }
core.info("Claude Code installed successfully"); core2.info("Claude Code installed successfully");
} catch (error2) { } catch (error2) {
throw new Error(`Failed to install Claude Code: ${error2}`); throw new Error(`Failed to install Claude Code: ${error2}`);
} }
@@ -25731,7 +25773,7 @@ var ClaudeAgent = class {
* Execute Claude Code with the given prompt * Execute Claude Code with the given prompt
*/ */
async execute(prompt) { async execute(prompt) {
core.info("Running Claude Code..."); core2.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" }));
@@ -25746,7 +25788,7 @@ var ClaudeAgent = class {
const env = { const env = {
ANTHROPIC_API_KEY: this.apiKey ANTHROPIC_API_KEY: this.apiKey
}; };
core.startGroup("\u{1F504} Run details"); core2.startGroup("\u{1F504} Run details");
this.runStats = { this.runStats = {
toolsUsed: 0, toolsUsed: 0,
turns: 0, turns: 0,
@@ -25780,11 +25822,11 @@ Stderr: ${result.stderr}`
); );
} }
const duration = Date.now() - this.runStats.startTime; const duration = Date.now() - this.runStats.startTime;
core.info( core2.info(
`\u{1F4CA} Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration` `\u{1F4CA} Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
); );
core.info("\u2705 Task complete."); core2.info("\u2705 Task complete.");
core.endGroup(); core2.endGroup();
return { return {
success: true, success: true,
output: finalResult, output: finalResult,
@@ -25797,7 +25839,7 @@ Stderr: ${result.stderr}`
}; };
} catch (error2) { } catch (error2) {
try { try {
core.endGroup(); core2.endGroup();
} catch { } catch {
} }
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error"; const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
@@ -25815,8 +25857,8 @@ function processJSONChunk(chunk, agent) {
switch (parsedChunk.type) { switch (parsedChunk.type) {
case "system": case "system":
if (parsedChunk.subtype === "init") { if (parsedChunk.subtype === "init") {
core.info(`\u{1F680} Starting Claude Code session...`); core2.info(`\u{1F680} Starting Claude Code session...`);
core.info( core2.info(
tableString([ tableString([
["model", parsedChunk.model], ["model", parsedChunk.model],
["cwd", parsedChunk.cwd], ["cwd", parsedChunk.cwd],
@@ -25845,7 +25887,7 @@ 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( core2.info(
boxString(content.text.trim(), { title: "Claude Code" }) boxString(content.text.trim(), { title: "Claude Code" })
); );
} }
@@ -25854,44 +25896,44 @@ function processJSONChunk(chunk, agent) {
agent.runStats.toolsUsed++; agent.runStats.toolsUsed++;
} }
const toolName = content.name; const toolName = content.name;
core.info(`\u2192 ${toolName}`); core2.info(`\u2192 ${toolName}`);
if (content.input) { if (content.input) {
const input = content.input; const input = content.input;
if (input.description) { if (input.description) {
core.info(` \u2514\u2500 ${input.description}`); core2.info(` \u2514\u2500 ${input.description}`);
} }
if (input.command) { if (input.command) {
core.info(` \u2514\u2500 command: ${input.command}`); core2.info(` \u2514\u2500 command: ${input.command}`);
} }
if (input.file_path) { if (input.file_path) {
core.info(` \u2514\u2500 file: ${input.file_path}`); core2.info(` \u2514\u2500 file: ${input.file_path}`);
} }
if (input.content) { if (input.content) {
const contentPreview = input.content.length > 100 ? `${input.content.substring(0, 100)}...` : input.content; const contentPreview = input.content.length > 100 ? `${input.content.substring(0, 100)}...` : input.content;
core.info(` \u2514\u2500 content: ${contentPreview}`); core2.info(` \u2514\u2500 content: ${contentPreview}`);
} }
if (input.query) { if (input.query) {
core.info(` \u2514\u2500 query: ${input.query}`); core2.info(` \u2514\u2500 query: ${input.query}`);
} }
if (input.pattern) { if (input.pattern) {
core.info(` \u2514\u2500 pattern: ${input.pattern}`); core2.info(` \u2514\u2500 pattern: ${input.pattern}`);
} }
if (input.url) { if (input.url) {
core.info(` \u2514\u2500 url: ${input.url}`); core2.info(` \u2514\u2500 url: ${input.url}`);
} }
if (input.edits && Array.isArray(input.edits)) { if (input.edits && Array.isArray(input.edits)) {
core.info(` \u2514\u2500 edits: ${input.edits.length} changes`); core2.info(` \u2514\u2500 edits: ${input.edits.length} changes`);
input.edits.forEach((edit, index) => { input.edits.forEach((edit, index) => {
if (edit.file_path) { if (edit.file_path) {
core.info(` ${index + 1}. ${edit.file_path}`); core2.info(` ${index + 1}. ${edit.file_path}`);
} }
}); });
} }
if (input.task) { if (input.task) {
core.info(` \u2514\u2500 task: ${input.task}`); core2.info(` \u2514\u2500 task: ${input.task}`);
} }
if (input.bash_command) { if (input.bash_command) {
core.info(` \u2514\u2500 bash_command: ${input.bash_command}`); core2.info(` \u2514\u2500 bash_command: ${input.bash_command}`);
} }
} }
} }
@@ -25903,7 +25945,7 @@ function processJSONChunk(chunk, agent) {
for (const content of parsedChunk.message.content) { for (const content of parsedChunk.message.content) {
if (content.type === "tool_result") { if (content.type === "tool_result") {
if (content.is_error) { if (content.is_error) {
core.warning(`\u274C Tool error: ${content.content}`); core2.warning(`\u274C Tool error: ${content.content}`);
} else { } else {
const _resultContent = content.content.trim(); const _resultContent = content.content.trim();
} }
@@ -25913,7 +25955,7 @@ function processJSONChunk(chunk, agent) {
break; break;
case "result": case "result":
if (parsedChunk.subtype === "success") { if (parsedChunk.subtype === "success") {
core.info( core2.info(
tableString([ tableString([
[ [
"Cost", "Cost",
@@ -25926,30 +25968,31 @@ function processJSONChunk(chunk, agent) {
]) ])
); );
} else { } else {
core.error(`\u274C Failed: ${parsedChunk.error || "Unknown error"}`); core2.error(`\u274C Failed: ${parsedChunk.error || "Unknown error"}`);
} }
break; break;
default: default:
core.debug(`\u{1F4E6} Unknown chunk type: ${parsedChunk.type}`); core2.debug(`\u{1F4E6} Unknown chunk type: ${parsedChunk.type}`);
break; break;
} }
} catch (error2) { } catch (error2) {
core.debug(`Failed to parse chunk: ${error2}`); core2.debug(`Failed to parse chunk: ${error2}`);
core.debug(`Raw chunk: ${chunk.substring(0, 200)}...`); core2.debug(`Raw chunk: ${chunk.substring(0, 200)}...`);
} }
} }
// main.ts // main.ts
async function main(params) { async function main(params) {
try { try {
const anthropicApiKey = params.anthropicApiKey || process.env.ANTHROPIC_API_KEY; const { inputs, env, cwd } = params;
if (!anthropicApiKey) { if (cwd !== process.cwd()) {
throw new Error("anthropic_api_key is required"); process.chdir(cwd);
} }
core2.info(`\u2192 Starting agent run with Claude Code`); Object.assign(process.env, env);
const agent = new ClaudeAgent({ apiKey: anthropicApiKey }); core3.info(`\u2192 Starting agent run with Claude Code`);
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
await agent.install(); await agent.install();
const result = await agent.execute(params.prompt); const result = await agent.execute(inputs.prompt);
if (!result.success) { if (!result.success) {
return { return {
success: false, success: false,
@@ -25973,30 +26016,37 @@ async function main(params) {
// entry.ts // entry.ts
async function run() { async function run() {
try { try {
const prompt = core3.getInput("prompt", { required: true }); const prompt = core4.getInput("prompt", { required: true });
const anthropicApiKey = core3.getInput("anthropic_api_key", { const anthropicApiKey = core4.getInput("anthropic_api_key");
required: true
});
if (!anthropicApiKey) {
throw new Error("anthropic_api_key is required");
}
if (!prompt) { if (!prompt) {
throw new Error("prompt is required"); throw new Error("prompt is required");
} }
const params = { const inputs = {
prompt, prompt,
anthropicApiKey anthropic_api_key: anthropicApiKey
};
const githubToken = core4.getInput("github_token") || process.env.GITHUB_TOKEN;
if (githubToken) {
inputs.github_token = githubToken;
}
const githubInstallationToken = core4.getInput("github_installation_token") || process.env.GITHUB_INSTALLATION_TOKEN;
if (githubInstallationToken) {
inputs.github_installation_token = githubInstallationToken;
} else {
await setupGitHubInstallationToken();
}
const params = {
inputs,
env: {},
cwd: process.cwd()
}; };
const result = await main(params); const result = await main(params);
core3.setOutput("status", result.success ? "success" : "failed");
core3.setOutput("prompt", prompt);
core3.setOutput("output", result.output || "");
if (!result.success) { if (!result.success) {
throw new Error(result.error || "Agent execution failed"); throw new Error(result.error || "Agent execution failed");
} }
} catch (error2) { } catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error occurred"; const errorMessage = error2 instanceof Error ? error2.message : "Unknown error occurred";
core3.setFailed(`Action failed: ${errorMessage}`); core4.setFailed(`Action failed: ${errorMessage}`);
} }
} }
run().catch((error2) => { run().catch((error2) => {
+28 -16
View File
@@ -7,43 +7,55 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { main } from "./main"; import { main } from "./main";
import { setupGitHubInstallationToken } from "./utils";
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
// Get inputs from GitHub Actions // Get inputs from GitHub Actions
const prompt = core.getInput("prompt", { required: true }); const prompt = core.getInput("prompt", { required: true });
const anthropicApiKey = core.getInput("anthropic_api_key", { const anthropicApiKey = core.getInput("anthropic_api_key");
required: true,
});
if (!anthropicApiKey) {
throw new Error("anthropic_api_key is required");
}
if (!prompt) { if (!prompt) {
throw new Error("prompt is required"); throw new Error("prompt is required");
} }
// Create params object // Create params object with new structure
const params = { const inputs: any = {
prompt, prompt,
anthropicApiKey, anthropic_api_key: anthropicApiKey,
};
// Add optional properties only if they exist
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN;
if (githubToken) {
inputs.github_token = githubToken;
}
const githubInstallationToken =
core.getInput("github_installation_token") || process.env.GITHUB_INSTALLATION_TOKEN;
if (githubInstallationToken) {
inputs.github_installation_token = githubInstallationToken;
} else {
// Setup GitHub installation token
await setupGitHubInstallationToken();
}
const params = {
inputs,
env: {} as Record<string, string>,
cwd: process.cwd(),
}; };
// Run the main function // Run the main function
const result = await main(params); const result = await main(params);
// Set outputs // TODO: Set outputs
core.setOutput("status", result.success ? "success" : "failed");
core.setOutput("prompt", prompt);
core.setOutput("output", result.output || "");
if (!result.success) { if (!result.success) {
throw new Error(result.error || "Agent execution failed"); throw new Error(result.error || "Agent execution failed");
} }
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
error instanceof Error ? error.message : "Unknown error occurred";
core.setFailed(`Action failed: ${errorMessage}`); core.setFailed(`Action failed: ${errorMessage}`);
} }
} }
+7 -1
View File
@@ -1,7 +1,13 @@
import type { MainParams } from "../main"; import type { MainParams } from "../main";
const testParams = { const testParams = {
prompt: "List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it." inputs: {
prompt:
"List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.",
anthropic_api_key: "sk-test-key",
},
env: {},
cwd: process.cwd(),
} satisfies MainParams; } satisfies MainParams;
export default testParams; export default testParams;
-25978
View File
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -3,6 +3,11 @@
* This exports the main function for programmatic usage * This exports the main function for programmatic usage
*/ */
export { main } from "./main";
export { ClaudeAgent } from "./agents"; export { ClaudeAgent } from "./agents";
export type { Agent, AgentConfig, AgentResult } from "./agents/types"; export type { Agent, AgentConfig, AgentResult } from "./agents/types";
export {
type ExecutionInputs,
type MainParams,
type MainResult,
main,
} from "./main";
+29 -11
View File
@@ -1,9 +1,24 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { ClaudeAgent } from "./agents"; import { ClaudeAgent } from "./agents";
export interface MainParams { // Expected environment variables that should be passed as inputs
export const EXPECTED_INPUTS: string[] = [
"ANTHROPIC_API_KEY",
"GITHUB_TOKEN",
"GITHUB_INSTALLATION_TOKEN"
];
export interface ExecutionInputs {
prompt: string; prompt: string;
anthropicApiKey?: string; anthropic_api_key: string;
github_token?: string;
github_installation_token?: string;
}
export interface MainParams {
inputs: ExecutionInputs;
env: Record<string, string>;
cwd: string;
} }
export interface MainResult { export interface MainResult {
@@ -12,24 +27,28 @@ export interface MainResult {
error?: string | undefined; error?: string | undefined;
} }
export async function main(params: MainParams): Promise<MainResult> { export async function main(params: MainParams): Promise<MainResult> {
try { try {
// Use provided API key or fall back to environment variable // Extract inputs from params
const anthropicApiKey = const { inputs, env, cwd } = params;
params.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
if (!anthropicApiKey) { // Set working directory if different from current
throw new Error("anthropic_api_key is required"); if (cwd !== process.cwd()) {
process.chdir(cwd);
} }
// Set environment variables
Object.assign(process.env, env);
core.info(`→ Starting agent run with Claude Code`); core.info(`→ Starting agent run with Claude Code`);
// Create and install the Claude agent // Create and install the Claude agent
const agent = new ClaudeAgent({ apiKey: anthropicApiKey }); const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
await agent.install(); await agent.install();
// Execute the agent with the prompt // Execute the agent with the prompt
const result = await agent.execute(params.prompt); const result = await agent.execute(inputs.prompt);
if (!result.success) { if (!result.success) {
return { return {
@@ -44,8 +63,7 @@ export async function main(params: MainParams): Promise<MainResult> {
output: result.output || "", output: result.output || "",
}; };
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
error instanceof Error ? error.message : "Unknown error occurred";
return { return {
success: false, success: false,
error: errorMessage, error: errorMessage,
+5 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.7", "version": "0.0.8",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
@@ -17,21 +17,22 @@
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"typecheck": "tsc --noEmit",
"build": "node esbuild.config.js", "build": "node esbuild.config.js",
"build:npm": "zshy", "build:npm": "zshy",
"build:dev": "node esbuild.config.js", "build:dev": "node esbuild.config.js",
"prepare": "husky", "prepare": "husky",
"play": "tsx --env-file=../.env play.ts" "play": "tsx play.ts"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.11.1",
"dotenv": "^17.2.2",
"execa": "^9.6.0", "execa": "^9.6.0",
"table": "^6.9.0" "table": "^6.9.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.0", "@types/node": "^20.10.0",
"commander": "^14.0.0", "commander": "^14.0.0",
"dotenv": "^17.2.2",
"esbuild": "^0.25.9", "esbuild": "^0.25.9",
"husky": "^9.0.0", "husky": "^9.0.0",
"typescript": "^5.3.0", "typescript": "^5.3.0",
+44 -75
View File
@@ -1,12 +1,15 @@
#!/usr/bin/env tsx import { existsSync, readFileSync } from "node:fs";
import { dirname, extname, join, resolve } from "node:path";
import { execSync } from "node:child_process";
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, resolve, extname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
import { Command } from "commander"; import { Command } from "commander";
import { config } from "dotenv";
import { main } from "./main"; import { main } from "./main";
import { runAct } from "./utils/act"; import { runAct } from "./utils/act";
import { setupTestRepo } from "./utils/setup";
// Load environment variables from .env file
config();
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -46,9 +49,7 @@ async function loadPrompt(filePath: string): Promise<string> {
const module = await import(fileUrl); const module = await import(fileUrl);
if (!module.default) { if (!module.default) {
throw new Error( throw new Error(`TypeScript file ${filePath} must have a default export`);
`TypeScript file ${filePath} must have a default export`,
);
} }
// If it's a string, use it directly // If it's a string, use it directly
@@ -66,16 +67,11 @@ async function loadPrompt(filePath: string): Promise<string> {
} }
default: default:
throw new Error( throw new Error(`Unsupported file type: ${ext}. Supported types: .txt, .json, .ts`);
`Unsupported file type: ${ext}. Supported types: .txt, .json, .ts`,
);
} }
} }
async function runPlay( async function runPlay(filePath: string, options: { act?: boolean }): Promise<void> {
filePath: string,
options: { act?: boolean },
): Promise<void> {
try { try {
// Load the prompt from the specified file // Load the prompt from the specified file
const prompt = await loadPrompt(filePath); const prompt = await loadPrompt(filePath);
@@ -85,56 +81,9 @@ async function runPlay(
console.log("🐳 Running with Docker/act..."); console.log("🐳 Running with Docker/act...");
runAct(prompt); runAct(prompt);
} else { } else {
// Clone the test repository and run directly // Setup test repository and run directly
const tempDir = join(process.cwd(), ".temp"); const tempDir = join(process.cwd(), ".temp");
const repoUrl = "git@github.com:pullfrogai/scratch.git"; setupTestRepo({ tempDir, forceClean: true });
// Remove existing temp directory if it exists
if (existsSync(tempDir)) {
console.log("🗑️ Removing existing .temp directory...");
rmSync(tempDir, { recursive: true, force: true });
}
// Clone the repository
console.log("📦 Cloning pullfrogai/scratch into .temp...");
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
// List of environment variables to copy to .temp
const envVarsToCopy = [
"ANTHROPIC_API_KEY",
"GITHUB_TOKEN",
// Add more environment variables here as needed
];
// Build .env content from the list
const envLines = envVarsToCopy
.map((varName) => `${varName}=${process.env[varName] || ""}`)
.join("\n");
const envPath = join(tempDir, ".env");
writeFileSync(envPath, envLines + "\n");
console.log("📝 Created .env file in .temp directory with:");
let hasRequiredVars = true;
envVarsToCopy.forEach((varName) => {
const hasValue = !!process.env[varName];
console.log(` - ${varName}: ${hasValue ? "✓" : "✗ (missing)"}`);
// Check for required variables
if (varName === "ANTHROPIC_API_KEY" && !hasValue) {
hasRequiredVars = false;
}
});
if (!hasRequiredVars) {
console.warn("\n⚠️ Warning: ANTHROPIC_API_KEY is not set or empty.");
console.warn(
" Please ensure you have a valid API key in your .env file.",
);
console.warn(
" Get your API key from: https://console.anthropic.com/api-keys\n",
);
}
// Change to the temp directory // Change to the temp directory
process.chdir(tempDir); process.chdir(tempDir);
@@ -145,8 +94,35 @@ async function runPlay(
console.log(prompt); console.log(prompt);
console.log("─".repeat(50)); console.log("─".repeat(50));
// Run main with the params object // Set environment variables from our .env for the action to use
const result = await main({ prompt }); const { EXPECTED_INPUTS } = await import("./main");
EXPECTED_INPUTS.forEach((inputName) => {
const value = process.env[inputName];
if (value) {
process.env[`INPUT_${inputName.toLowerCase()}`] = value;
}
});
// Run main with the new params structure
const inputs: any = {
prompt,
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
};
// Add optional properties only if they exist
if (process.env.GITHUB_TOKEN) {
inputs.github_token = process.env.GITHUB_TOKEN;
}
if (process.env.GITHUB_INSTALLATION_TOKEN) {
inputs.github_installation_token = process.env.GITHUB_INSTALLATION_TOKEN;
}
const result = await main({
inputs,
env: process.env as Record<string, string>,
cwd: process.cwd(),
});
if (result.success) { if (result.success) {
console.log("✅ Test completed successfully"); console.log("✅ Test completed successfully");
@@ -171,15 +147,8 @@ program
.name("play") .name("play")
.description("Test the Pullfrog action with various prompts") .description("Test the Pullfrog action with various prompts")
.version("1.0.0") .version("1.0.0")
.argument( .argument("[file]", "Prompt file to use (.txt, .json, or .ts)", "fixtures/basic.txt")
"[file]", .option("--act", "Use Docker/act to run the action instead of running directly")
"Prompt file to use (.txt, .json, or .ts)",
"fixtures/basic.txt",
)
.option(
"--act",
"Use Docker/act to run the action instead of running directly",
)
.action(async (file: string, options: { act?: boolean }) => { .action(async (file: string, options: { act?: boolean }) => {
await runPlay(file, options); await runPlay(file, options);
}); });
+4 -4
View File
@@ -9,8 +9,11 @@ importers:
.: .:
dependencies: dependencies:
'@actions/core': '@actions/core':
specifier: ^1.10.1 specifier: ^1.11.1
version: 1.11.1 version: 1.11.1
dotenv:
specifier: ^17.2.2
version: 17.2.2
execa: execa:
specifier: ^9.6.0 specifier: ^9.6.0
version: 9.6.0 version: 9.6.0
@@ -24,9 +27,6 @@ importers:
commander: commander:
specifier: ^14.0.0 specifier: ^14.0.0
version: 14.0.0 version: 14.0.0
dotenv:
specifier: ^17.2.2
version: 17.2.2
esbuild: esbuild:
specifier: ^0.25.9 specifier: ^0.25.9
version: 0.25.9 version: 0.25.9
+22 -41
View File
@@ -1,59 +1,36 @@
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs"; import { existsSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { config, parse } from "dotenv"; import { config } from "dotenv";
import { buildAction, setupTestRepo } from "./setup";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
// Environment variables that should be passed as secrets to the workflow
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
export function runAct(prompt: string): void { export function runAct(prompt: string): void {
// First, ensure the scratch repo is cloned
const tempDir = join(__dirname, "..", ".temp"); const tempDir = join(__dirname, "..", ".temp");
const actionPath = join(__dirname, "..");
// Check if .temp exists and either reset it or clone it
if (existsSync(tempDir)) {
console.log("📦 Resetting existing .temp repository...");
execSync("git reset --hard HEAD && git clean -fd", {
cwd: tempDir,
stdio: "inherit",
});
} else {
console.log("📦 Cloning pullfrogai/scratch into .temp...");
const repoUrl = "git@github.com:pullfrogai/scratch.git";
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
}
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
const envPath = join(__dirname, "..", "..", ".env"); const envPath = join(__dirname, "..", "..", ".env");
// Load environment variables into process // Setup test repository
setupTestRepo({ tempDir });
// Load environment variables
config({ path: envPath }); config({ path: envPath });
// Parse environment variables from .env file to get keys // Build action bundles
let envVars: string[] = []; buildAction(actionPath);
try {
const content = readFileSync(envPath, "utf8");
const parsed = parse(content);
envVars = Object.keys(parsed);
} catch (error) {
console.warn(
`Warning: Could not read .env file: ${(error as Error).message}`,
);
}
// Build fresh bundles with esbuild const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
const actionPath = join(__dirname, "..");
console.log("🔨 Building fresh bundles with esbuild...");
execSync("node esbuild.config.js", {
cwd: actionPath,
stdio: "inherit",
});
// Create minimal dist for act (avoids pnpm symlink issues) // Create minimal dist for act (avoids pnpm symlink issues)
const distPath = join(actionPath, ".act-dist"); const distPath = join(actionPath, ".act-dist");
console.log("📦 Creating minimal distribution for act..."); console.log("📦 Creating minimal distribution for act...");
execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: true }); execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: "/bin/bash" });
// Copy only necessary files (bundled, no node_modules needed) // Copy only necessary files (bundled, no node_modules needed)
["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => { ["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => {
@@ -79,11 +56,15 @@ export function runAct(prompt: string): void {
`pullfrog/action@v0=${distPath}`, // Use minimal dist without symlinks `pullfrog/action@v0=${distPath}`, // Use minimal dist without symlinks
]; ];
// Add all environment variables as secrets (without values) // Add environment variables as secrets that will be available to the workflow
envVars.forEach((key) => { ENV_VARS.forEach((key) => {
actCommandParts.push("-s", key); if (process.env[key]) {
actCommandParts.push("-s", key);
}
}); });
// We only need the specific ENV_VARS, no need to add other variables
const actCommand = actCommandParts.join(" "); const actCommand = actCommandParts.join(" ");
console.log("🚀 Running act with prompt:"); console.log("🚀 Running act with prompt:");
+56
View File
@@ -0,0 +1,56 @@
import * as core from "@actions/core";
/**
* Setup GitHub installation token for the action
*/
export async function setupGitHubInstallationToken(): Promise<string> {
// Check if we have an installation token from inputs or environment
const inputToken = core.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
const existingToken = inputToken || envToken;
if (existingToken) {
core.info("Using provided GitHub installation token");
return existingToken;
}
core.info("No cached installation token found, generating OIDC token...");
try {
// Generate OIDC token for our API
const oidcToken = await core.getIDToken("pullfrog-api");
core.info("OIDC token generated successfully");
// Exchange OIDC token for installation token
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
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();
core.info(
`Installation token obtained for ${tokenData.owner}/${tokenData.repository || "all repositories"}`
);
// Set the token as an environment variable for this run
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"}`
);
}
}
+2
View File
@@ -1,3 +1,5 @@
export * from "./files"; export * from "./files";
export * from "./github";
export * from "./setup";
export * from "./subprocess"; export * from "./subprocess";
export * from "./table"; export * from "./table";
+51
View File
@@ -0,0 +1,51 @@
import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
export interface SetupOptions {
tempDir: string;
repoUrl?: string;
forceClean?: boolean;
}
/**
* Setup the test repository for running actions
*/
export function setupTestRepo(options: SetupOptions): void {
const {
tempDir,
repoUrl = "git@github.com:pullfrogai/scratch.git",
forceClean = false,
} = options;
// Handle existing temp directory
if (existsSync(tempDir)) {
if (forceClean) {
console.log("🗑️ Removing existing .temp directory...");
rmSync(tempDir, { recursive: true, force: true });
// Clone the repository
console.log("📦 Cloning pullfrogai/scratch into .temp...");
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
} else {
console.log("📦 Resetting existing .temp repository...");
execSync("git reset --hard HEAD && git clean -fd", {
cwd: tempDir,
stdio: "inherit",
});
}
} else {
console.log("📦 Cloning pullfrogai/scratch into .temp...");
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
}
}
/**
* Build the action bundles
*/
export function buildAction(actionPath: string): void {
console.log("🔨 Building fresh bundles with esbuild...");
execSync("node esbuild.config.js", {
cwd: actionPath,
stdio: "inherit",
});
}