dtu-github-actions 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +29 -0
- package/dist/ephemeral.d.ts +16 -0
- package/dist/ephemeral.js +48 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +326 -0
- package/dist/server/logger.d.ts +4 -0
- package/dist/server/logger.js +56 -0
- package/dist/server/routes/actions/generators.d.ts +25 -0
- package/dist/server/routes/actions/generators.js +313 -0
- package/dist/server/routes/actions/index.d.ts +2 -0
- package/dist/server/routes/actions/index.js +575 -0
- package/dist/server/routes/artifacts.d.ts +2 -0
- package/dist/server/routes/artifacts.js +332 -0
- package/dist/server/routes/cache.d.ts +2 -0
- package/dist/server/routes/cache.js +230 -0
- package/dist/server/routes/cache.test.d.ts +1 -0
- package/dist/server/routes/cache.test.js +229 -0
- package/dist/server/routes/dtu.d.ts +3 -0
- package/dist/server/routes/dtu.js +141 -0
- package/dist/server/routes/github.d.ts +2 -0
- package/dist/server/routes/github.js +109 -0
- package/dist/server/start.d.ts +1 -0
- package/dist/server/start.js +22 -0
- package/dist/server/store.d.ts +44 -0
- package/dist/server/store.js +100 -0
- package/dist/server.js +1179 -0
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +322 -0
- package/dist/simulate.d.ts +1 -0
- package/dist/simulate.js +47 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# DTU
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const configSchema: z.ZodObject<{
|
|
3
|
+
/**
|
|
4
|
+
* The secret used to sign GitHub webhooks.
|
|
5
|
+
* Hardcoded for local-only mock usage.
|
|
6
|
+
*/
|
|
7
|
+
GITHUB_WEBHOOK_SECRET: z.ZodDefault<z.ZodString>;
|
|
8
|
+
/**
|
|
9
|
+
* The internal URL where the DTU Mock Server is reachable.
|
|
10
|
+
* Simulation scripts seed this server.
|
|
11
|
+
*/
|
|
12
|
+
DTU_URL: z.ZodDefault<z.ZodString>;
|
|
13
|
+
/**
|
|
14
|
+
* The port the DTU Mock Server listens on.
|
|
15
|
+
*/
|
|
16
|
+
DTU_PORT: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
/**
|
|
18
|
+
* Directory where cache archives should be stored.
|
|
19
|
+
*/
|
|
20
|
+
DTU_CACHE_DIR: z.ZodDefault<z.ZodString>;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
GITHUB_WEBHOOK_SECRET: string;
|
|
23
|
+
DTU_URL: string;
|
|
24
|
+
DTU_PORT: number;
|
|
25
|
+
DTU_CACHE_DIR: string;
|
|
26
|
+
}, {
|
|
27
|
+
GITHUB_WEBHOOK_SECRET?: string | undefined;
|
|
28
|
+
DTU_URL?: string | undefined;
|
|
29
|
+
DTU_PORT?: number | undefined;
|
|
30
|
+
DTU_CACHE_DIR?: string | undefined;
|
|
31
|
+
}>;
|
|
32
|
+
export type Config = z.infer<typeof configSchema>;
|
|
33
|
+
export declare const config: {
|
|
34
|
+
GITHUB_WEBHOOK_SECRET: string;
|
|
35
|
+
DTU_URL: string;
|
|
36
|
+
DTU_PORT: number;
|
|
37
|
+
DTU_CACHE_DIR: string;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const configSchema = z.object({
|
|
5
|
+
/**
|
|
6
|
+
* The secret used to sign GitHub webhooks.
|
|
7
|
+
* Hardcoded for local-only mock usage.
|
|
8
|
+
*/
|
|
9
|
+
GITHUB_WEBHOOK_SECRET: z.string().min(1).default("agent-ci-local"),
|
|
10
|
+
/**
|
|
11
|
+
* The internal URL where the DTU Mock Server is reachable.
|
|
12
|
+
* Simulation scripts seed this server.
|
|
13
|
+
*/
|
|
14
|
+
DTU_URL: z.string().url().default("http://localhost:8910"),
|
|
15
|
+
/**
|
|
16
|
+
* The port the DTU Mock Server listens on.
|
|
17
|
+
*/
|
|
18
|
+
DTU_PORT: z.coerce.number().default(8910),
|
|
19
|
+
/**
|
|
20
|
+
* Directory where cache archives should be stored.
|
|
21
|
+
*/
|
|
22
|
+
DTU_CACHE_DIR: z.string().default(() => path.join(os.tmpdir(), "dtu_cache")),
|
|
23
|
+
});
|
|
24
|
+
export const config = configSchema.parse({
|
|
25
|
+
GITHUB_WEBHOOK_SECRET: process.env.GITHUB_WEBHOOK_SECRET,
|
|
26
|
+
DTU_URL: process.env.DTU_URL,
|
|
27
|
+
DTU_PORT: process.env.DTU_PORT,
|
|
28
|
+
DTU_CACHE_DIR: process.env.DTU_CACHE_DIR,
|
|
29
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface EphemeralDtu {
|
|
2
|
+
/** Full URL including port, e.g. "http://127.0.0.1:49823" */
|
|
3
|
+
url: string;
|
|
4
|
+
port: number;
|
|
5
|
+
/** Shut down the ephemeral DTU server. */
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Start an ephemeral in-process DTU server on a random OS-assigned port.
|
|
10
|
+
*
|
|
11
|
+
* Each call creates an independent server instance — no shared state between
|
|
12
|
+
* calls. Typical startup overhead is ~50ms.
|
|
13
|
+
*
|
|
14
|
+
* @param cacheDir Where cache archives should be stored (e.g. `os.tmpdir()/agent-ci/<repo>/cache/dtu`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function startEphemeralDtu(cacheDir: string): Promise<EphemeralDtu>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { setCacheDir } from "./server/store.js";
|
|
3
|
+
import { bootstrapAndReturnApp } from "./server/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Start an ephemeral in-process DTU server on a random OS-assigned port.
|
|
6
|
+
*
|
|
7
|
+
* Each call creates an independent server instance — no shared state between
|
|
8
|
+
* calls. Typical startup overhead is ~50ms.
|
|
9
|
+
*
|
|
10
|
+
* @param cacheDir Where cache archives should be stored (e.g. `os.tmpdir()/agent-ci/<repo>/cache/dtu`).
|
|
11
|
+
*/
|
|
12
|
+
export async function startEphemeralDtu(cacheDir) {
|
|
13
|
+
// Override the cache directory before bootstrapping so the store writes
|
|
14
|
+
// archives to the repo-scoped path rather than the global tmp dir.
|
|
15
|
+
setCacheDir(cacheDir);
|
|
16
|
+
// Build the Polka app with all routes registered.
|
|
17
|
+
const app = await bootstrapAndReturnApp({ reset: false });
|
|
18
|
+
// Wrap the Polka request handler in a plain Node.js HTTP server so we can
|
|
19
|
+
// bind to port 0 (OS-assigned) and get back the actual port.
|
|
20
|
+
const server = http.createServer((req, res) => {
|
|
21
|
+
// Polka exposes its composed handler as `app.handler`.
|
|
22
|
+
app.handler(req, res);
|
|
23
|
+
});
|
|
24
|
+
const port = await new Promise((resolve, reject) => {
|
|
25
|
+
server.listen(0, "0.0.0.0", () => {
|
|
26
|
+
const addr = server.address();
|
|
27
|
+
if (!addr || typeof addr === "string") {
|
|
28
|
+
return reject(new Error("Unexpected server address type"));
|
|
29
|
+
}
|
|
30
|
+
resolve(addr.port);
|
|
31
|
+
});
|
|
32
|
+
server.on("error", reject);
|
|
33
|
+
});
|
|
34
|
+
const url = `http://127.0.0.1:${port}`;
|
|
35
|
+
return {
|
|
36
|
+
url,
|
|
37
|
+
port,
|
|
38
|
+
close() {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
// Force-close all existing connections (HTTP keep-alive etc.)
|
|
41
|
+
// so the server shuts down immediately instead of waiting for
|
|
42
|
+
// idle connections to drain.
|
|
43
|
+
server.closeAllConnections();
|
|
44
|
+
server.close(() => resolve());
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import polka from "polka";
|
|
2
|
+
import bodyParser from "body-parser";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { config } from "../config.js";
|
|
7
|
+
import { state } from "./store.js";
|
|
8
|
+
import { setupDtuLogging, getDtuLogPath } from "./logger.js";
|
|
9
|
+
// Routes
|
|
10
|
+
import { registerDtuRoutes } from "./routes/dtu.js";
|
|
11
|
+
import { registerGithubRoutes } from "./routes/github.js";
|
|
12
|
+
import { registerActionRoutes } from "./routes/actions/index.js";
|
|
13
|
+
import { registerCacheRoutes } from "./routes/cache.js";
|
|
14
|
+
import { registerArtifactRoutes } from "./routes/artifacts.js";
|
|
15
|
+
async function terminateOldProcess() {
|
|
16
|
+
// Kill existing process on DTU port
|
|
17
|
+
try {
|
|
18
|
+
await execa("kill", ["-9", `$(lsof -t -i:${config.DTU_PORT})`], { shell: true, reject: false });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore error if no process found
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function bootstrapAndReturnApp(options) {
|
|
25
|
+
const shouldReset = options?.reset ?? true;
|
|
26
|
+
setupDtuLogging();
|
|
27
|
+
if (shouldReset) {
|
|
28
|
+
state.reset();
|
|
29
|
+
await terminateOldProcess();
|
|
30
|
+
}
|
|
31
|
+
const app = polka();
|
|
32
|
+
// Polka's listen() does: server.on('request', this.handler). So wrapping app.handler
|
|
33
|
+
// is the correct place to normalize double-slashes BEFORE polka parses req.url into req.path.
|
|
34
|
+
// ACTIONS_CACHE_URL ends with '/' and routes start with '/' — producing '//_apis/...' paths.
|
|
35
|
+
const originalHandler = app.handler.bind(app);
|
|
36
|
+
app.handler = (req, res, info) => {
|
|
37
|
+
if (req.url?.includes("//")) {
|
|
38
|
+
req.url = req.url.replace(/\/{2,}/g, "/");
|
|
39
|
+
}
|
|
40
|
+
originalHandler(req, res, info);
|
|
41
|
+
};
|
|
42
|
+
// Request timing middleware
|
|
43
|
+
app.use((req, res, next) => {
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
const origEnd = res.end.bind(res);
|
|
46
|
+
res.end = (...args) => {
|
|
47
|
+
const ms = Date.now() - start;
|
|
48
|
+
const url = req.url || "";
|
|
49
|
+
if (!url.includes("/logs/") && !url.includes("/feed") && !url.includes("/lines")) {
|
|
50
|
+
console.log(`[DTU] ${req.method} ${url} (${ms}ms)`);
|
|
51
|
+
}
|
|
52
|
+
return origEnd(...args);
|
|
53
|
+
};
|
|
54
|
+
next();
|
|
55
|
+
});
|
|
56
|
+
app.use(bodyParser.json({ limit: "50mb" }));
|
|
57
|
+
// Raw parsers for logs and cache uploads
|
|
58
|
+
app.use(bodyParser.text({ type: ["text/plain"], limit: "50mb" }));
|
|
59
|
+
app.use(bodyParser.raw({
|
|
60
|
+
type: ["application/octet-stream", "application/zip", "application/xml", "text/xml"],
|
|
61
|
+
limit: "500mb",
|
|
62
|
+
}));
|
|
63
|
+
// Routes
|
|
64
|
+
registerDtuRoutes(app);
|
|
65
|
+
registerGithubRoutes(app);
|
|
66
|
+
registerCacheRoutes(app);
|
|
67
|
+
registerArtifactRoutes(app);
|
|
68
|
+
registerActionRoutes(app);
|
|
69
|
+
app.post("/_apis/distributedtask/hubs/:hub/plans/:planId/logs/:logId", (req, res) => {
|
|
70
|
+
let text = "";
|
|
71
|
+
if (typeof req.body === "string") {
|
|
72
|
+
text = req.body;
|
|
73
|
+
}
|
|
74
|
+
else if (Buffer.isBuffer(req.body)) {
|
|
75
|
+
text = req.body.toString("utf-8");
|
|
76
|
+
}
|
|
77
|
+
if (text) {
|
|
78
|
+
const planId = req.params.planId;
|
|
79
|
+
const logDir = state.planToLogDir.get(planId);
|
|
80
|
+
if (logDir) {
|
|
81
|
+
let content = "";
|
|
82
|
+
for (const rawLine of text.split("\n")) {
|
|
83
|
+
const line = rawLine.trimEnd();
|
|
84
|
+
if (!line) {
|
|
85
|
+
content += "\n";
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const stripped = line
|
|
89
|
+
.replace(/^\uFEFF?\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, "")
|
|
90
|
+
.replace(/^\uFEFF/, "");
|
|
91
|
+
if (!stripped || stripped.startsWith("##[") || stripped.startsWith("[command]")) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
content += stripped + "\n";
|
|
95
|
+
}
|
|
96
|
+
if (content) {
|
|
97
|
+
try {
|
|
98
|
+
const stepName = state.recordToStepName.get(String(req.params.logId)) || req.params.logId;
|
|
99
|
+
const stepsDir = path.join(logDir, "steps");
|
|
100
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
101
|
+
fs.appendFileSync(path.join(stepsDir, `${stepName}.log`), content);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* best-effort */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const lineCount = text ? text.split("\n").filter((l) => l.trim()).length : 0;
|
|
110
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
111
|
+
res.end(JSON.stringify({
|
|
112
|
+
id: parseInt(req.params.logId),
|
|
113
|
+
path: `logs/${req.params.logId}`,
|
|
114
|
+
lineCount,
|
|
115
|
+
createdOn: new Date().toISOString(),
|
|
116
|
+
}));
|
|
117
|
+
});
|
|
118
|
+
app.put("/_apis/distributedtask/hubs/:hub/plans/:planId/logs/:logId", (req, res) => {
|
|
119
|
+
let text = "";
|
|
120
|
+
if (typeof req.body === "string") {
|
|
121
|
+
text = req.body;
|
|
122
|
+
}
|
|
123
|
+
else if (Buffer.isBuffer(req.body)) {
|
|
124
|
+
text = req.body.toString("utf-8");
|
|
125
|
+
}
|
|
126
|
+
if (text) {
|
|
127
|
+
const planId = req.params.planId;
|
|
128
|
+
const logDir = state.planToLogDir.get(planId);
|
|
129
|
+
if (logDir) {
|
|
130
|
+
let content = "";
|
|
131
|
+
for (const rawLine of text.split("\n")) {
|
|
132
|
+
const line = rawLine.trimEnd();
|
|
133
|
+
if (!line) {
|
|
134
|
+
content += "\n";
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const stripped = line
|
|
138
|
+
.replace(/^\uFEFF?\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, "")
|
|
139
|
+
.replace(/^\uFEFF/, "");
|
|
140
|
+
if (!stripped || stripped.startsWith("##[") || stripped.startsWith("[command]")) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
content += stripped + "\n";
|
|
144
|
+
}
|
|
145
|
+
if (content) {
|
|
146
|
+
try {
|
|
147
|
+
const stepName = state.recordToStepName.get(String(req.params.logId)) || req.params.logId;
|
|
148
|
+
const stepsDir = path.join(logDir, "steps");
|
|
149
|
+
fs.mkdirSync(stepsDir, { recursive: true });
|
|
150
|
+
fs.appendFileSync(path.join(stepsDir, `${stepName}.log`), content);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* best-effort */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const lineCount = text ? text.split("\n").filter((l) => l.trim()).length : 0;
|
|
159
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
160
|
+
res.end(JSON.stringify({
|
|
161
|
+
id: parseInt(req.params.logId),
|
|
162
|
+
path: `logs/${req.params.logId}`,
|
|
163
|
+
lineCount,
|
|
164
|
+
createdOn: new Date().toISOString(),
|
|
165
|
+
}));
|
|
166
|
+
});
|
|
167
|
+
// Global OPTIONS (CORS & Discovery)
|
|
168
|
+
app.options("/*", (req, res) => {
|
|
169
|
+
res.writeHead(200, {
|
|
170
|
+
"Access-Control-Allow-Origin": "*",
|
|
171
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS, PATCH, PUT, DELETE",
|
|
172
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-TFS-FedAuthRedirect, X-VSS-E2EID, X-TFS-Session",
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
});
|
|
175
|
+
const responseValue = [
|
|
176
|
+
{
|
|
177
|
+
id: "A8C47E17-4D56-4A56-92BB-DE7EA7DC65BE",
|
|
178
|
+
area: "distributedtask",
|
|
179
|
+
resourceName: "pools",
|
|
180
|
+
routeTemplate: "_apis/distributedtask/pools/{poolId}",
|
|
181
|
+
resourceVersion: 1,
|
|
182
|
+
minVersion: "1.0",
|
|
183
|
+
maxVersion: "9.0",
|
|
184
|
+
releasedVersion: "9.0",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "E298EF32-5878-4CAB-993C-043836571F42",
|
|
188
|
+
area: "distributedtask",
|
|
189
|
+
resourceName: "agents",
|
|
190
|
+
routeTemplate: "_apis/distributedtask/pools/{poolId}/agents/{agentId}",
|
|
191
|
+
resourceVersion: 1,
|
|
192
|
+
minVersion: "1.0",
|
|
193
|
+
maxVersion: "9.0",
|
|
194
|
+
releasedVersion: "9.0",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "C3A054F6-7A8A-49C0-944E-3A8E5D7ADFD7",
|
|
198
|
+
area: "distributedtask",
|
|
199
|
+
resourceName: "messages",
|
|
200
|
+
routeTemplate: "_apis/distributedtask/pools/{poolId}/messages",
|
|
201
|
+
resourceVersion: 1,
|
|
202
|
+
minVersion: "1.0",
|
|
203
|
+
maxVersion: "9.0",
|
|
204
|
+
releasedVersion: "9.0",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "134E239E-2DF3-4794-A6F6-24F1F19EC8DC",
|
|
208
|
+
area: "distributedtask",
|
|
209
|
+
resourceName: "sessions",
|
|
210
|
+
routeTemplate: "_apis/distributedtask/pools/{poolId}/sessions/{sessionId}",
|
|
211
|
+
resourceVersion: 1,
|
|
212
|
+
minVersion: "1.0",
|
|
213
|
+
maxVersion: "9.0",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "83597576-CC2C-453C-BEA6-2882AE6A1653",
|
|
217
|
+
area: "distributedtask",
|
|
218
|
+
resourceName: "timelines",
|
|
219
|
+
routeTemplate: "_apis/distributedtask/timelines/{timelineId}",
|
|
220
|
+
resourceVersion: 1,
|
|
221
|
+
minVersion: "1.0",
|
|
222
|
+
maxVersion: "9.0",
|
|
223
|
+
releasedVersion: "9.0",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "27d7f831-88c1-4719-8ca1-6a061dad90eb",
|
|
227
|
+
area: "distributedtask",
|
|
228
|
+
resourceName: "actiondownloadinfo",
|
|
229
|
+
routeTemplate: "_apis/distributedtask/hubs/{hubName}/plans/{planId}/actiondownloadinfo",
|
|
230
|
+
resourceVersion: 1,
|
|
231
|
+
minVersion: "1.0",
|
|
232
|
+
maxVersion: "6.0",
|
|
233
|
+
releasedVersion: "6.0",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "858983e4-19bd-4c5e-864c-507b59b58b12",
|
|
237
|
+
area: "distributedtask",
|
|
238
|
+
resourceName: "feed",
|
|
239
|
+
routeTemplate: "_apis/distributedtask/hubs/{hubName}/plans/{planId}/timelines/{timelineId}/records/{recordId}/feed",
|
|
240
|
+
resourceVersion: 1,
|
|
241
|
+
minVersion: "1.0",
|
|
242
|
+
maxVersion: "9.0",
|
|
243
|
+
releasedVersion: "9.0",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "46f5667d-263a-4684-91b1-dff7fdcf64e2",
|
|
247
|
+
area: "distributedtask",
|
|
248
|
+
resourceName: "logs",
|
|
249
|
+
routeTemplate: "_apis/distributedtask/hubs/{hubName}/plans/{planId}/logs/{logId}",
|
|
250
|
+
resourceVersion: 1,
|
|
251
|
+
minVersion: "1.0",
|
|
252
|
+
maxVersion: "9.0",
|
|
253
|
+
releasedVersion: "9.0",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "8893BC5B-35B2-4BE7-83CB-99E683551DB4",
|
|
257
|
+
area: "distributedtask",
|
|
258
|
+
resourceName: "records",
|
|
259
|
+
routeTemplate: "_apis/distributedtask/timelines/{timelineId}/records/{recordId}",
|
|
260
|
+
resourceVersion: 1,
|
|
261
|
+
minVersion: "1.0",
|
|
262
|
+
maxVersion: "9.0",
|
|
263
|
+
releasedVersion: "9.0",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "FC825784-C92A-4299-9221-998A02D1B54F",
|
|
267
|
+
area: "distributedtask",
|
|
268
|
+
resourceName: "jobrequests",
|
|
269
|
+
routeTemplate: "_apis/distributedtask/jobrequests/{jobId}",
|
|
270
|
+
resourceVersion: 1,
|
|
271
|
+
minVersion: "1.0",
|
|
272
|
+
maxVersion: "9.0",
|
|
273
|
+
releasedVersion: "9.0",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "0A1EFD25-ABDA-43BD-9629-6C7BDD2E0D60",
|
|
277
|
+
area: "distributedtask",
|
|
278
|
+
resourceName: "jobinstances",
|
|
279
|
+
routeTemplate: "_apis/distributedtask/jobinstances/{jobId}",
|
|
280
|
+
resourceVersion: 1,
|
|
281
|
+
minVersion: "1.0",
|
|
282
|
+
maxVersion: "9.0",
|
|
283
|
+
releasedVersion: "9.0",
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
res.end(JSON.stringify({ count: responseValue.length, value: responseValue }));
|
|
287
|
+
});
|
|
288
|
+
// Health and root APIs discovery
|
|
289
|
+
app.get("/_apis", (req, res) => {
|
|
290
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
291
|
+
res.end(JSON.stringify({ value: [] }));
|
|
292
|
+
});
|
|
293
|
+
app.get("/", (req, res) => {
|
|
294
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
295
|
+
res.end(JSON.stringify({ status: "online", seededJobs: state.jobs.size }));
|
|
296
|
+
});
|
|
297
|
+
app.head("/", (req, res) => {
|
|
298
|
+
res.writeHead(200);
|
|
299
|
+
res.end();
|
|
300
|
+
});
|
|
301
|
+
// Catch-all 404 with payload dumping
|
|
302
|
+
app.all("/*", (req, res) => {
|
|
303
|
+
console.log(`[DTU] 404 Not Found: ${req.method} ${req.url} (Details in 404.log)`);
|
|
304
|
+
let logContent = `\\n--- [${new Date().toISOString()}] 404 Not Found: ${req.method} ${req.url} ---\\n`;
|
|
305
|
+
logContent += `Headers: ${JSON.stringify(req.headers, null, 2)}\\n`;
|
|
306
|
+
if (req.body && Object.keys(req.body).length > 0) {
|
|
307
|
+
logContent += `Body (parsed JSON): ${JSON.stringify(req.body, null, 2)}\\n`;
|
|
308
|
+
}
|
|
309
|
+
else if (typeof req.body === "string" && req.body.length > 0) {
|
|
310
|
+
logContent += `Body (raw text): ${req.body.substring(0, 500)}${req.body.length > 500 ? "..." : ""}\\n`;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const logDir = path.dirname(getDtuLogPath());
|
|
314
|
+
if (!fs.existsSync(logDir)) {
|
|
315
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
316
|
+
}
|
|
317
|
+
fs.appendFileSync(path.join(logDir, "404.log"), logContent);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
/* best-effort */
|
|
321
|
+
}
|
|
322
|
+
res.writeHead(404);
|
|
323
|
+
res.end("Not Found (DTU Mock)");
|
|
324
|
+
});
|
|
325
|
+
return app;
|
|
326
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
5
|
+
export const DTU_ROOT = path.resolve(fileURLToPath(import.meta.url), "..", "..", "..", "..");
|
|
6
|
+
let dtuLogsDir = process.env.DTU_LOGS_DIR ?? path.join(DTU_ROOT, "_", "logs");
|
|
7
|
+
let dtuLogPath = path.join(dtuLogsDir, "dtu-server.log");
|
|
8
|
+
export function setWorkingDirectory(dir) {
|
|
9
|
+
dtuLogsDir = path.join(dir, "logs");
|
|
10
|
+
dtuLogPath = path.join(dtuLogsDir, "dtu-server.log");
|
|
11
|
+
}
|
|
12
|
+
export function getDtuLogPath() {
|
|
13
|
+
return dtuLogPath;
|
|
14
|
+
}
|
|
15
|
+
let logStream = null;
|
|
16
|
+
const _origLog = console.log.bind(console);
|
|
17
|
+
const _origWarn = console.warn.bind(console);
|
|
18
|
+
const _origError = console.error.bind(console);
|
|
19
|
+
/** Check if DTU debug output should appear on the terminal. */
|
|
20
|
+
function isDtuDebugEnabled() {
|
|
21
|
+
const patterns = (process.env.DEBUG || "")
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((s) => s.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
return patterns.some((p) => minimatch("agent-ci:dtu", p) || minimatch("agent-ci:*", p));
|
|
26
|
+
}
|
|
27
|
+
export function setupDtuLogging() {
|
|
28
|
+
fs.mkdirSync(dtuLogsDir, { recursive: true });
|
|
29
|
+
logStream = fs.createWriteStream(dtuLogPath, { flags: "a" });
|
|
30
|
+
const dtuDebug = isDtuDebugEnabled();
|
|
31
|
+
// console.log/warn: always write to log file, only show in terminal when debug is on
|
|
32
|
+
console.log = (...args) => {
|
|
33
|
+
if (dtuDebug) {
|
|
34
|
+
_origLog(...args);
|
|
35
|
+
}
|
|
36
|
+
writeToLog(...args);
|
|
37
|
+
};
|
|
38
|
+
console.warn = (...args) => {
|
|
39
|
+
if (dtuDebug) {
|
|
40
|
+
_origWarn(...args);
|
|
41
|
+
}
|
|
42
|
+
writeToLog("[WARN]", ...args);
|
|
43
|
+
};
|
|
44
|
+
// console.error: always show in terminal (real errors)
|
|
45
|
+
console.error = (...args) => {
|
|
46
|
+
_origError(...args);
|
|
47
|
+
writeToLog("[ERROR]", ...args);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function writeToLog(...args) {
|
|
51
|
+
if (!logStream) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const line = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
|
|
55
|
+
logStream.write(`${new Date().toISOString()} ${line}\n`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MessageResponse } from "../../../types.js";
|
|
2
|
+
export declare function toContextData(obj: any): any;
|
|
3
|
+
export declare function toTemplateTokenMapping(obj: {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
}): object;
|
|
6
|
+
/**
|
|
7
|
+
* Convert a container definition { image, env?, ports?, volumes?, options? }
|
|
8
|
+
* into a TemplateToken MappingToken that the runner's EvaluateJobContainer expects.
|
|
9
|
+
*
|
|
10
|
+
* Format:
|
|
11
|
+
* { type: 2, map: [{ Key: "image", Value: "alpine:3.19" }, ...] }
|
|
12
|
+
*
|
|
13
|
+
* Nested:
|
|
14
|
+
* env → MappingToken (type 2)
|
|
15
|
+
* ports/volumes → SequenceToken (type 1) of StringTokens
|
|
16
|
+
* options → StringToken (bare string)
|
|
17
|
+
*/
|
|
18
|
+
export declare function toContainerTemplateToken(container: {
|
|
19
|
+
image: string;
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
ports?: string[];
|
|
22
|
+
volumes?: string[];
|
|
23
|
+
options?: string;
|
|
24
|
+
}): object;
|
|
25
|
+
export declare function createJobResponse(jobId: string, payload: any, baseUrl: string, planId: string): MessageResponse;
|