-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathsigned-commit-artefacts.ts
More file actions
176 lines (165 loc) · 5.43 KB
/
Copy pathsigned-commit-artefacts.ts
File metadata and controls
176 lines (165 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import { readFileSync } from "node:fs";
import type { SignedCommitResult } from "@posthog/git/signed-commit";
import { PostHogAPIClient } from "./posthog-api";
const SANDBOX_ENV_FILE = "/tmp/agent-env";
const SANDBOX_OAUTH_ENV_FILE = "/tmp/agent-oauth-env";
/**
* Best-effort "commit hook": after a successful signed-commit push, record one `commit`
* artefact per pushed commit on every signal report the task is associated with, so the
* report's work log shows exactly what landed. Attribution is deterministic — the artefact
* endpoint reads the `X-PostHog-Task-Id` header, never the model.
*
* Credentials come from the sandbox environment (`POSTHOG_API_URL` /
* `POSTHOG_PERSONAL_API_KEY` / `POSTHOG_PROJECT_ID`), preferring the dedicated live agentsh
* OAuth file for the key so a mid-session token refresh is picked up — the same pattern as
* `resolveGithubToken`. Never throws: a failed artefact post must not fail the commit that
* already landed.
*/
interface SandboxPosthogApi {
apiUrl: string;
apiKey: string;
projectId: number;
}
function readSandboxEnvFile(envFilePath: string): Record<string, string> {
try {
const raw = readFileSync(envFilePath, "utf8");
const env: Record<string, string> = {};
for (const entry of raw.split("\0")) {
const eq = entry.indexOf("=");
if (eq > 0) {
env[entry.slice(0, eq)] = entry.slice(eq + 1);
}
}
return env;
} catch {
// No env file (local/desktop or test) — fall back to the process env only.
return {};
}
}
function readSandboxOauthToken(oauthEnvFilePath: string): string | undefined {
let raw: string;
try {
raw = readFileSync(oauthEnvFilePath, "utf8");
} catch {
// The dedicated credential channel is mandatory. Missing and unreadable
// files both fail closed.
return undefined;
}
// The backend revokes OAuth access by truncating this managed file. Its
// presence is authoritative even when empty.
if (raw.trim() === "") {
return "";
}
const oauthEnv = readSandboxEnvFile(oauthEnvFilePath);
return oauthEnv.POSTHOG_PERSONAL_API_KEY ?? "";
}
export function resolveSandboxPosthogApi(
env: Record<string, string | undefined> = process.env,
envFilePath: string = SANDBOX_ENV_FILE,
oauthEnvFilePath: string = SANDBOX_OAUTH_ENV_FILE,
): SandboxPosthogApi | undefined {
const fileEnv = readSandboxEnvFile(envFilePath);
const oauthToken = readSandboxOauthToken(oauthEnvFilePath);
const apiUrl = fileEnv.POSTHOG_API_URL ?? env.POSTHOG_API_URL;
const projectId = Number(
fileEnv.POSTHOG_PROJECT_ID ?? env.POSTHOG_PROJECT_ID,
);
if (!apiUrl || !oauthToken || !Number.isFinite(projectId) || projectId <= 0) {
return undefined;
}
return { apiUrl, apiKey: oauthToken, projectId };
}
export function createSandboxPosthogClient(
env?: Record<string, string | undefined>,
envFilePath?: string,
oauthEnvFilePath?: string,
): PostHogAPIClient | undefined {
const api = resolveSandboxPosthogApi(env, envFilePath, oauthEnvFilePath);
if (!api) {
return undefined;
}
return new PostHogAPIClient({
apiUrl: api.apiUrl,
projectId: api.projectId,
getApiKey: () => api.apiKey,
});
}
export async function reportCommitArtefacts(opts: {
taskId: string | undefined;
result: SignedCommitResult;
/** Commit headline — the same for every chunk of a split payload. */
message: string;
env?: Record<string, string | undefined>;
envFilePath?: string;
oauthEnvFilePath?: string;
}): Promise<void> {
const { taskId, result, message } = opts;
if (!taskId) {
return; // Local/desktop run — no task to attribute or associate through.
}
try {
const client = createSandboxPosthogClient(
opts.env,
opts.envFilePath,
opts.oauthEnvFilePath,
);
if (!client) {
return; // No sandbox PostHog credentials — nothing to report to.
}
const reportIds = await client.getSignalReportIdsForTask(taskId);
for (const reportId of reportIds) {
for (const commit of result.commits) {
try {
await client.createSignalReportArtefact(reportId, taskId, {
artefact_type: "commit",
content: {
repository: result.repository,
branch: result.branch,
commit_sha: commit.sha,
message,
},
});
} catch (err) {
warn(
`failed to record commit ${commit.sha} on report ${reportId}: ${err}`,
);
}
}
}
} catch (err) {
warn(`failed to record commit artefacts: ${err}`);
}
}
export async function reportTaskRunBranch(opts: {
taskId: string | undefined;
taskRunId: string | undefined;
branch: string;
env?: Record<string, string | undefined>;
envFilePath?: string;
oauthEnvFilePath?: string;
}): Promise<void> {
if (!opts.taskId || !opts.taskRunId) {
return;
}
try {
const client = createSandboxPosthogClient(
opts.env,
opts.envFilePath,
opts.oauthEnvFilePath,
);
if (!client) {
return;
}
await client.updateTaskRun(opts.taskId, opts.taskRunId, {
branch: opts.branch,
output: { head_branch: opts.branch },
});
} catch (err) {
warn(`failed to attach branch ${opts.branch} to task run: ${err}`);
}
}
// stderr directly (not console) — this also runs inside the Codex stdio MCP child,
// where stdout is the protocol channel.
function warn(message: string): void {
process.stderr.write(`[signed-commit-artefacts] ${message}\n`);
}