Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/agents/agent/channels/github.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { githubChannel } from "eve/channels/github";

import { githubCredentials } from "../lib/github.js";

export default githubChannel({
botName: process.env.GITHUB_APP_SLUG
botName: process.env.GITHUB_APP_SLUG ?? "turborepo-eve-agent",
credentials: githubCredentials
});
1 change: 1 addition & 0 deletions apps/agents/agent/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ You are the Turborepo examples maintenance agent. Your job is to keep the exampl
- Do not downgrade or hold a direct dependency below the latest stable registry version because of compatibility concerns. If latest breaks, migrate the example until latest works.
- Version bumps are not enough. When upgrading a framework, toolchain, or library, migrate the example to that ecosystem's current best-practice configuration and APIs instead of preserving deprecated patterns.
- Do not stop with checkpoint summaries, partial progress reports, or "I'll continue" messages. For broad examples updates, keep working until every example has been updated, lockfiles are regenerated, and relevant non-persistent validation tasks have passed or produced a concrete external blocker.
- When an automated schedule produces changes, create a draft pull request with `create_pull_request`. Use an `agents/weekly-examples-YYYY-MM-DD` branch, a Conventional Commit title, and include the validation results in the pull request body.
66 changes: 66 additions & 0 deletions apps/agents/agent/lib/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { getVercelOidcToken } from "@vercel/oidc";
import type { GitHubChannelCredentials } from "eve/channels/github";

const TOKEN_EXPIRY_SAFETY_WINDOW_MS = 30_000;

interface GitHubStsResponse {
token: string;
expires_at: string;
}

let cachedToken: { token: string; expiresAt: number } | null = null;

export const githubCredentials: GitHubChannelCredentials = {
installationToken: getGitHubToken
};

export async function getGitHubToken(): Promise<string> {
if (
cachedToken !== null &&
cachedToken.expiresAt - TOKEN_EXPIRY_SAFETY_WINDOW_MS > Date.now()
) {
return cachedToken.token;
}

const oidcToken = await getVercelOidcToken();
const tokenExchangeUrl = process.env.GITHUB_TOKEN_EXCHANGE_URL;
if (!tokenExchangeUrl) {
throw new Error("GITHUB_TOKEN_EXCHANGE_URL is not configured.");
}
const response = await fetch(tokenExchangeUrl, {
method: "POST",
headers: {
authorization: `Bearer ${oidcToken}`,
"content-type": "application/json"
},
body: JSON.stringify({
owner: "vercel",
repo: "turborepo",
permissions: {
contents: "write",
pull_requests: "write"
}
})
});

if (!response.ok) {
const detail = await response.text().catch(() => "");
throw new Error(
`GitHub token exchange failed (${response.status} ${response.statusText})${detail === "" ? "" : `: ${detail}`}`
);
}

const body = (await response.json()) as Partial<GitHubStsResponse>;
if (typeof body.token !== "string" || body.token === "") {
throw new Error("GitHub token exchange did not include a token.");
}
const expiresAt = Date.parse(String(body.expires_at));
if (Number.isNaN(expiresAt)) {
throw new Error(
`GitHub token exchange had invalid expires_at: ${String(body.expires_at)}`
);
}

cachedToken = { token: body.token, expiresAt };
return body.token;
}
Loading
Loading