mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(check): resolve build log url for failed check suites via GitHub API
check_suite payloads carry no details_url; the build log lives on the underlying check_run. For failed suites, fetch the suite's check runs with an installation token and inject the first run's details_url (preferring the Cloudflare run) as a Build Log field.
This commit is contained in:
parent
b2db616865
commit
449739ee97
3 changed files with 79 additions and 1 deletions
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import { getDriver } from "../drivers";
|
import { getDriver } from "../drivers";
|
||||||
import type { SendResult } from "../drivers/types";
|
import type { SendResult } from "../drivers/types";
|
||||||
import type { DispatchFailure, DispatchSummary } from "../queue/delivery";
|
import type { DispatchFailure, DispatchSummary } from "../queue/delivery";
|
||||||
|
import { getCheckSuiteBuildLogUrl } from "../github/check-run";
|
||||||
|
|
||||||
/** One dispatch attempt (route × target), collected for the group webhook log. */
|
/** One dispatch attempt (route × target), collected for the group webhook log. */
|
||||||
interface DispatchAttempt {
|
interface DispatchAttempt {
|
||||||
|
|
@ -170,6 +171,28 @@ export async function dispatchEvent(
|
||||||
const tr = trMap.get(group?.lang ?? "en")!;
|
const tr = trMap.get(group?.lang ?? "en")!;
|
||||||
const showEmoji = group?.emoji !== false;
|
const showEmoji = group?.emoji !== false;
|
||||||
const message = formatEvent(route, event, tr, showEmoji);
|
const message = formatEvent(route, event, tr, showEmoji);
|
||||||
|
if (event.event === "check_suite") {
|
||||||
|
const suite = event.payload.check_suite as {
|
||||||
|
conclusion?: string;
|
||||||
|
check_runs_url?: string;
|
||||||
|
} | undefined;
|
||||||
|
if (suite?.conclusion === "failure" && suite.check_runs_url) {
|
||||||
|
const buildLogUrl = await getCheckSuiteBuildLogUrl(
|
||||||
|
suite.check_runs_url,
|
||||||
|
env.GITHUB_APP_ID,
|
||||||
|
env.GITHUB_PRIVATE_KEY,
|
||||||
|
event.installationId,
|
||||||
|
);
|
||||||
|
if (buildLogUrl) {
|
||||||
|
message.fields = message.fields ?? [];
|
||||||
|
message.fields.push({
|
||||||
|
name: translate("fields.build_log", {}, undefined, tr),
|
||||||
|
value: `[${translate("fields.build_log", {}, undefined, tr)}](${buildLogUrl})`,
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (group?.forgeSources?.length) {
|
if (group?.forgeSources?.length) {
|
||||||
message.forge = forgeInfo(event, group.forgeSources);
|
message.forge = forgeInfo(event, group.forgeSources);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
server/lib/github/check-run.ts
Normal file
55
server/lib/github/check-run.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { createAppJwt } from "./oauth";
|
||||||
|
|
||||||
|
const API_VERSION = "2022-11-28";
|
||||||
|
const CLOUDFLARE_SLUG = "cloudflare-workers-and-pages";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a check suite's build log URL. The `check_suite` webhook payload
|
||||||
|
* carries no `details_url` of its own — only the underlying `check_run` does
|
||||||
|
* (Cloudflare sets it to the build log page). For a failed suite we fetch the
|
||||||
|
* suite's check runs via the GitHub API and return the first run's
|
||||||
|
* `details_url`, preferring the Cloudflare run when present.
|
||||||
|
*
|
||||||
|
* Returns undefined when the App credentials are missing, the call fails, or
|
||||||
|
* no run exposes a `details_url`. Best-effort: callers must not depend on it.
|
||||||
|
*/
|
||||||
|
export async function getCheckSuiteBuildLogUrl(
|
||||||
|
checkRunsUrl: string | undefined,
|
||||||
|
appId: string | undefined,
|
||||||
|
privateKey: string | undefined,
|
||||||
|
installationId: number | undefined,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!checkRunsUrl || !appId || !privateKey || !installationId) return undefined;
|
||||||
|
try {
|
||||||
|
const jwt = await createAppJwt(appId, privateKey);
|
||||||
|
const tokRes = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": API_VERSION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!tokRes.ok) return undefined;
|
||||||
|
const { token } = (await tokRes.json()) as { token?: string };
|
||||||
|
if (!token) return undefined;
|
||||||
|
|
||||||
|
const runsRes = await fetch(checkRunsUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": API_VERSION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!runsRes.ok) return undefined;
|
||||||
|
const data = (await runsRes.json()) as {
|
||||||
|
check_runs?: Array<{ details_url?: string; app?: { slug?: string } }>;
|
||||||
|
};
|
||||||
|
const runs = data.check_runs ?? [];
|
||||||
|
const cloudflare = runs.find((r) => r.app?.slug === CLOUDFLARE_SLUG && r.details_url);
|
||||||
|
const any = runs.find((r) => r.details_url);
|
||||||
|
return (cloudflare ?? any)?.details_url;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ function pemToBinary(pem: string): ArrayBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GitHub App JWT (RS256, PKCS#8 PEM key), valid ~10 minutes. */
|
/** GitHub App JWT (RS256, PKCS#8 PEM key), valid ~10 minutes. */
|
||||||
async function createAppJwt(appId: string, privateKey: string): Promise<string> {
|
export async function createAppJwt(appId: string, privateKey: string): Promise<string> {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
||||||
const payload = b64url(JSON.stringify({ iat: now - 60, exp: now + 600, iss: appId }));
|
const payload = b64url(JSON.stringify({ iat: now - 60, exp: now + 600, iss: appId }));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue