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
4 changes: 2 additions & 2 deletions packages/docs/src/content/docs/extend/github-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,14 +360,14 @@ One pull request: `owner/repo#number`.
<details class="resource-event">
<summary><code>pull_request.checks.failed</code></summary>

One or more checks failed.
A check suite finished with failure or timeout. Trusted data includes the PR, full head SHA, suite id/url, and failed check-run ids/urls when enrichment works. Failed check names are untrusted provider content.

</details>

<details class="resource-event">
<summary><code>pull_request.checks.recovered</code></summary>

Previously failing checks recovered.
A check suite finished successfully after a failure. Trusted data includes the PR, full head SHA, and suite id/url. This is for one suite only. It does not mean the whole PR is green.

</details>

Expand Down
21 changes: 20 additions & 1 deletion packages/junior-evals/evals/agent/subscriptions.eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,26 @@ describeEval("Resource Event Subscriptions", slackEvals, (it) => {
label: "GitHub PR getsentry/junior#691",
identifier: "getsentry/junior#691",
trustedSummary:
'GitHub PR getsentry/junior#691 checks failed on workflow "test" for commit abcdef123456.',
"GitHub PR getsentry/junior#691 checks failed (1) for abcdef123456.",
data: {
repo: "getsentry/junior",
pullRequest: 691,
headSha: "abcdef1234567890abcdef1234567890abcdef12",
scope: "check_suite",
suiteConclusion: "failure",
checkSuiteId: 42,
checkSuiteUrl:
"https://github.com/getsentry/junior/commit/abcdef1234567890abcdef1234567890abcdef12/checks?check_suite_id=42",
failingChecks: [
{
checkRunId: 1,
conclusion: "failure",
htmlUrl: "https://github.com/getsentry/junior/actions/runs/1",
},
],
},
untrustedText:
"Failed checks:\n- test: https://github.com/getsentry/junior/actions/runs/1",
}),
],
criteria: rubric({
Expand Down
2 changes: 2 additions & 0 deletions packages/junior-evals/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,7 @@ interface ResourceEventNotificationOptions {
subscriptionId?: string;
thread?: ThreadOverrides;
trustedSummary: string;
data?: Record<string, unknown>;
untrustedText?: string;
}

Expand Down Expand Up @@ -1044,6 +1045,7 @@ function resourceEventNotificationText(
{
eventType: opts.eventType,
trustedSummary: opts.trustedSummary,
data: opts.data,
untrustedText: opts.untrustedText,
},
);
Expand Down
1 change: 1 addition & 0 deletions packages/junior-github/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ In GitHub:
- Contents: Read and write
- Pull requests: Read and write
- Actions: Read and write
- Checks: Read
- Deployments: Read
- Workflows: Write
- Metadata: Read
Expand Down
9 changes: 9 additions & 0 deletions packages/junior-github/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import type { GitHubDb } from "./db/database.js";
import { buildGitHubOutcomeReport } from "./outcomes/report.js";
import { classifyGitHubPullRequestCommitComposition } from "./pull-request-outcomes/commit-composition.js";
import { loadFailingChecksForSuite } from "./webhooks/check-suite-enrichment.js";
import {
additionalActorCoauthorTrailers,
configureGit,
Expand Down Expand Up @@ -775,6 +776,14 @@ export function githubPlugin(
},
db: ctx.db as GitHubDb,
installationId: () => readEnv(installationIdEnv),
loadFailingChecks: async (body) =>
await loadFailingChecksForSuite({
appIdEnv,
body,
installationIdEnv,
log: ctx.log,
privateKeyEnv,
}),
log: ctx.log,
resourceEvents: ctx.resourceEvents,
webhookSecret: () => readEnv("GITHUB_WEBHOOK_SECRET"),
Expand Down
56 changes: 56 additions & 0 deletions packages/junior-github/src/webhooks/check-suite-enrichment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
githubRequest,
issueInstallationToken,
} from "../credential-support.js";
import {
parseCheckSuiteEnrichmentTarget,
selectFailingChecks,
type GitHubFailingCheck,
} from "./resource-events.js";

function checkRunsFromResponse(value: unknown): unknown[] {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return [];
}
const checkRuns = (value as { check_runs?: unknown }).check_runs;
return Array.isArray(checkRuns) ? checkRuns : [];
}

/** Load failed check-run names and urls for one failed check suite. */
export async function loadFailingChecksForSuite(args: {
appIdEnv: string;
body: unknown;
installationIdEnv: string;
log?: { error(message: string, metadata?: Record<string, unknown>): void };
privateKeyEnv: string;
}): Promise<GitHubFailingCheck[] | undefined> {
const target = parseCheckSuiteEnrichmentTarget(args.body);
if (!target) return undefined;

try {
const token = await issueInstallationToken({
appIdEnv: args.appIdEnv,
installationIdEnv: args.installationIdEnv,
permissions: { checks: "read" },
privateKeyEnv: args.privateKeyEnv,
repositories: [target.repoName],
});
// Load runs for this suite only. Commit-wide latest runs mix other apps.
const response = await githubRequest(
"https://api.github.com",
`/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repoName)}/check-suites/${target.checkSuiteId}/check-runs?filter=latest&per_page=100`,
{ token: token.token },
);
const failing = selectFailingChecks(checkRunsFromResponse(response), {
checkSuiteId: target.checkSuiteId,
});
return failing.length > 0 ? failing : undefined;
} catch (error) {
args.log?.error("GitHub check suite enrichment failed", {
checkSuiteId: target.checkSuiteId,
errorType: error instanceof Error ? error.name : "UnknownError",
repository: `${target.owner}/${target.repoName}`,
});
return undefined;
}
}
11 changes: 10 additions & 1 deletion packages/junior-github/src/webhooks/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import {
normalizeGitHubPullRequestLinkedIssues,
normalizeGitHubPullRequestOutcome,
} from "./pull-request-outcome.js";
import { normalizeGitHubResourceEvents } from "./resource-events.js";
import {
normalizeGitHubResourceEvents,
type GitHubFailingCheck,
} from "./resource-events.js";

/** Verify GitHub's SHA-256 signature against the untouched request body. */
function verifyGitHubSignature(
Expand Down Expand Up @@ -85,6 +88,7 @@ export function createGitHubWebhookRoute(args: {
}): Promise<GitHubPullRequestCommitComposition | undefined>;
db: GitHubDb;
installationId(): string | undefined;
loadFailingChecks?(body: unknown): Promise<GitHubFailingCheck[] | undefined>;
log?: Pick<PluginLogger, "error">;
resourceEvents: ResourceEventPublisher;
webhookSecret(): string | undefined;
Expand Down Expand Up @@ -187,10 +191,15 @@ export function createGitHubWebhookRoute(args: {
)
: false;

const failingChecks =
eventName === "check_suite" && args.loadFailingChecks
? await args.loadFailingChecks(body)
: undefined;
const resourceEvents = normalizeGitHubResourceEvents({
body,
deliveryId,
eventName,
failingChecks,
});
for (const event of resourceEvents) {
await args.resourceEvents.publish(event);
Expand Down
Loading
Loading