b748355cbe
* add wiki/betterstack.md documenting log querying, request-ID grouping, and MCP usage
Made-with: Cursor
* fix webhook race conditions: separate runId assignment from data updates
the workflow_run webhook handler had a race where concurrent handlers assigned
the same runId to different pending records. the loser's P2002 silently dropped
data updates (jobId, status, completedAt). fix by splitting into two steps:
assignRunId() handles the race-safe unique assignment, then data updates always
target where: { runId } so they hit the correct record regardless of who won.
also downgrade R2 ObjectLockedByBucketPolicy errors from error to warn level
since duplicate webhook deliveries writing the same key is expected under load.
Made-with: Cursor
* homepage copy refresh + fix skills CLI installation
- update hero to "Agent x GitHub" with new subtagline
- rewrite intro paragraphs: workflow, harness capabilities, billing
- add feature sections: bash isolation, headless browser, MCP tools
- update FAQ answers, footer attribution, free-for-oss copy
- update APP_DESCRIPTION for SEO
- fix skills install: use npx from tmpdir instead of local binary
(the bundled action has no node_modules; running npx from tmpdir
avoids project .npmrc with pnpm settings breaking binary resolution)
- instruct agents to use markdown image syntax in upload_file tool
- start dependency installation eagerly from main.ts
- include event title in task instructions
Made-with: Cursor
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { type } from "arktype";
|
|
import { fileTypeFromBuffer } from "file-type";
|
|
import { apiFetch } from "../utils/apiFetch.ts";
|
|
import type { ToolContext } from "./server.ts";
|
|
import { execute, tool } from "./shared.ts";
|
|
|
|
const UploadFileParams = type({
|
|
path: type.string.describe("absolute path to file to upload"),
|
|
});
|
|
|
|
export function UploadFileTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "upload_file",
|
|
description:
|
|
"upload a file to get a permanent public URL. use for screenshots, artifacts, or any files you want to reference in PRs/comments. max 10MB, images/text/archives allowed. when embedding uploaded images in comments or PR bodies, always use markdown image syntax: ",
|
|
parameters: UploadFileParams,
|
|
execute: execute(async (params) => {
|
|
// read file from disk eagerly on purpose to avoid its content being changed by the time it's uploaded
|
|
const buffer = fs.readFileSync(params.path);
|
|
const filename = path.basename(params.path);
|
|
const contentLength = buffer.length;
|
|
|
|
const fileType = await fileTypeFromBuffer(buffer);
|
|
const contentType = fileType?.mime || "application/octet-stream";
|
|
|
|
const response = await apiFetch({
|
|
path: "/api/upload/signed-url",
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.apiToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
filename,
|
|
contentType,
|
|
contentLength,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`failed to get upload URL: ${error}`);
|
|
}
|
|
|
|
const { uploadUrl, publicUrl, contentDisposition } = (await response.json()) as {
|
|
uploadUrl: string;
|
|
publicUrl: string;
|
|
contentDisposition?: string | undefined;
|
|
};
|
|
|
|
const uploadResponse = await fetch(uploadUrl, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": contentType,
|
|
// should be set automatically, but given this header is signed it's better to be explicit
|
|
"Content-Length": String(contentLength),
|
|
...(contentDisposition && { "Content-Disposition": contentDisposition }),
|
|
},
|
|
body: buffer,
|
|
});
|
|
|
|
if (!uploadResponse.ok) {
|
|
throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
|
|
}
|
|
|
|
return { success: true, publicUrl, filename, contentLength, contentType };
|
|
}),
|
|
});
|
|
}
|