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
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
/** Build a minimal JWT whose `scp` claim satisfies @actions/artifact v2. */
|
|
3
|
+
function createMockJwt(planId, jobId) {
|
|
4
|
+
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
|
|
5
|
+
const payload = Buffer.from(JSON.stringify({
|
|
6
|
+
orchid: "123",
|
|
7
|
+
scp: `Actions.Results:${planId}:${jobId}`,
|
|
8
|
+
})).toString("base64url");
|
|
9
|
+
return `${header}.${payload}.mock-signature`;
|
|
10
|
+
}
|
|
11
|
+
// Helper to convert JS objects to ContextData
|
|
12
|
+
export function toContextData(obj) {
|
|
13
|
+
if (typeof obj === "string") {
|
|
14
|
+
return { t: 0, s: obj };
|
|
15
|
+
}
|
|
16
|
+
if (typeof obj === "boolean") {
|
|
17
|
+
return { t: 3, b: obj };
|
|
18
|
+
}
|
|
19
|
+
if (typeof obj === "number") {
|
|
20
|
+
return { t: 4, n: obj };
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(obj)) {
|
|
23
|
+
return {
|
|
24
|
+
t: 1,
|
|
25
|
+
a: obj.map(toContextData),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (typeof obj === "object" && obj !== null) {
|
|
29
|
+
return {
|
|
30
|
+
t: 2,
|
|
31
|
+
d: Object.entries(obj).map(([k, v]) => ({ k, v: toContextData(v) })),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Handle null or undefined
|
|
35
|
+
return { t: 0, s: "" };
|
|
36
|
+
}
|
|
37
|
+
// Build a TemplateToken MappingToken in the format ActionStep.Inputs expects.
|
|
38
|
+
// TemplateTokenJsonConverter uses "type" key (integer) NOT the contextData "t" key.
|
|
39
|
+
// TokenType.Mapping = 2. Items are serialized as {Key: scalarToken, Value: templateToken}.
|
|
40
|
+
// Strings without file/line/col are serialized as bare string values.
|
|
41
|
+
export function toTemplateTokenMapping(obj) {
|
|
42
|
+
const entries = Object.entries(obj);
|
|
43
|
+
if (entries.length === 0) {
|
|
44
|
+
return { type: 2 };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
type: 2,
|
|
48
|
+
map: entries.map(([k, v]) => ({ Key: k, Value: v })),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Convert a container definition { image, env?, ports?, volumes?, options? }
|
|
53
|
+
* into a TemplateToken MappingToken that the runner's EvaluateJobContainer expects.
|
|
54
|
+
*
|
|
55
|
+
* Format:
|
|
56
|
+
* { type: 2, map: [{ Key: "image", Value: "alpine:3.19" }, ...] }
|
|
57
|
+
*
|
|
58
|
+
* Nested:
|
|
59
|
+
* env → MappingToken (type 2)
|
|
60
|
+
* ports/volumes → SequenceToken (type 1) of StringTokens
|
|
61
|
+
* options → StringToken (bare string)
|
|
62
|
+
*/
|
|
63
|
+
export function toContainerTemplateToken(container) {
|
|
64
|
+
const map = [];
|
|
65
|
+
map.push({ Key: "image", Value: container.image });
|
|
66
|
+
if (container.env && Object.keys(container.env).length > 0) {
|
|
67
|
+
map.push({
|
|
68
|
+
Key: "env",
|
|
69
|
+
Value: {
|
|
70
|
+
type: 2,
|
|
71
|
+
map: Object.entries(container.env).map(([k, v]) => ({ Key: k, Value: v })),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (container.ports && container.ports.length > 0) {
|
|
76
|
+
map.push({
|
|
77
|
+
Key: "ports",
|
|
78
|
+
Value: { type: 1, seq: container.ports },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (container.volumes && container.volumes.length > 0) {
|
|
82
|
+
map.push({
|
|
83
|
+
Key: "volumes",
|
|
84
|
+
Value: { type: 1, seq: container.volumes },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (container.options) {
|
|
88
|
+
map.push({ Key: "options", Value: container.options });
|
|
89
|
+
}
|
|
90
|
+
return { type: 2, map };
|
|
91
|
+
}
|
|
92
|
+
export function createJobResponse(jobId, payload, baseUrl, planId) {
|
|
93
|
+
const mappedSteps = (payload.steps || []).map((step, index) => {
|
|
94
|
+
const inputsObj = step.Inputs || (step.run ? { script: step.run } : {});
|
|
95
|
+
const s = {
|
|
96
|
+
id: step.Id || step.id || crypto.randomUUID(),
|
|
97
|
+
name: step.Name || step.name || `step-${index}`,
|
|
98
|
+
displayName: step.DisplayName || step.Name || step.name || `step-${index}`,
|
|
99
|
+
type: (step.Type || "Action").toLowerCase(),
|
|
100
|
+
reference: (() => {
|
|
101
|
+
const refTypeSource = step.Reference?.Type || "Script";
|
|
102
|
+
const refTypeString = refTypeSource.toLowerCase();
|
|
103
|
+
let typeInt = 3;
|
|
104
|
+
if (refTypeString === "repository") {
|
|
105
|
+
typeInt = 1;
|
|
106
|
+
}
|
|
107
|
+
else if (refTypeString === "container") {
|
|
108
|
+
typeInt = 2;
|
|
109
|
+
}
|
|
110
|
+
const reference = { type: typeInt };
|
|
111
|
+
if (typeInt === 1 && step.Reference) {
|
|
112
|
+
reference.name = step.Reference.Name;
|
|
113
|
+
reference.ref = step.Reference.Ref;
|
|
114
|
+
reference.repositoryType = step.Reference.RepositoryType || "GitHub";
|
|
115
|
+
reference.path = step.Reference.Path || "";
|
|
116
|
+
}
|
|
117
|
+
return reference;
|
|
118
|
+
})(),
|
|
119
|
+
// inputs is TemplateToken (MappingToken). Must use {"type": 2, "map": [...]} format.
|
|
120
|
+
inputs: toTemplateTokenMapping(inputsObj),
|
|
121
|
+
contextData: step.ContextData || toContextData({}),
|
|
122
|
+
// condition must be explicit — null Condition causes NullReferenceException in EvaluateStepIf
|
|
123
|
+
condition: step.condition || "success()",
|
|
124
|
+
};
|
|
125
|
+
return s;
|
|
126
|
+
});
|
|
127
|
+
const repoFullName = payload.repository?.full_name || payload.githubRepo || "";
|
|
128
|
+
const ownerName = payload.repository?.owner?.login || "redwoodjs";
|
|
129
|
+
const repoName = payload.repository?.name || repoFullName.split("/")[1] || "";
|
|
130
|
+
const workspacePath = `/home/runner/_work/${repoName}/${repoName}`;
|
|
131
|
+
const Variables = {
|
|
132
|
+
// Standard GitHub Actions environment variables — always set by real runners.
|
|
133
|
+
// CI=true is required by many scripts that branch on CI vs local (e.g. default DB_HOST).
|
|
134
|
+
CI: { Value: "true", IsSecret: false },
|
|
135
|
+
GITHUB_CI: { Value: "true", IsSecret: false },
|
|
136
|
+
GITHUB_ACTIONS: { Value: "true", IsSecret: false },
|
|
137
|
+
// Runner metadata
|
|
138
|
+
RUNNER_OS: { Value: "Linux", IsSecret: false },
|
|
139
|
+
RUNNER_ARCH: { Value: "X64", IsSecret: false },
|
|
140
|
+
RUNNER_NAME: { Value: "oa-local-runner", IsSecret: false },
|
|
141
|
+
RUNNER_TEMP: { Value: "/tmp/runner", IsSecret: false },
|
|
142
|
+
RUNNER_TOOL_CACHE: { Value: "/opt/hostedtoolcache", IsSecret: false },
|
|
143
|
+
// Workflow / run metadata
|
|
144
|
+
GITHUB_RUN_ID: { Value: "1", IsSecret: false },
|
|
145
|
+
GITHUB_RUN_NUMBER: { Value: "1", IsSecret: false },
|
|
146
|
+
GITHUB_JOB: { Value: payload.name || "local-job", IsSecret: false },
|
|
147
|
+
GITHUB_EVENT_NAME: { Value: "push", IsSecret: false },
|
|
148
|
+
GITHUB_REF_NAME: { Value: "main", IsSecret: false },
|
|
149
|
+
GITHUB_WORKFLOW: { Value: payload.workflowName || "local-workflow", IsSecret: false },
|
|
150
|
+
GITHUB_WORKSPACE: { Value: workspacePath, IsSecret: false },
|
|
151
|
+
// Repository / identity
|
|
152
|
+
"system.github.token": { Value: "fake-token", IsSecret: true },
|
|
153
|
+
"system.github.job": { Value: "local-job", IsSecret: false },
|
|
154
|
+
"system.github.repository": { Value: repoFullName, IsSecret: false },
|
|
155
|
+
"github.repository": { Value: repoFullName, IsSecret: false },
|
|
156
|
+
"github.actor": { Value: ownerName, IsSecret: false },
|
|
157
|
+
"github.sha": {
|
|
158
|
+
Value: payload.headSha && payload.headSha !== "HEAD"
|
|
159
|
+
? payload.headSha
|
|
160
|
+
: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
161
|
+
IsSecret: false,
|
|
162
|
+
},
|
|
163
|
+
"github.ref": { Value: "refs/heads/main", IsSecret: false },
|
|
164
|
+
repository: { Value: repoFullName, IsSecret: false },
|
|
165
|
+
GITHUB_REPOSITORY: { Value: repoFullName, IsSecret: false },
|
|
166
|
+
GITHUB_ACTOR: { Value: ownerName, IsSecret: false },
|
|
167
|
+
GITHUB_SHA: {
|
|
168
|
+
Value: payload.headSha && payload.headSha !== "HEAD"
|
|
169
|
+
? payload.headSha
|
|
170
|
+
: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
171
|
+
IsSecret: false,
|
|
172
|
+
},
|
|
173
|
+
"build.repository.name": { Value: repoFullName, IsSecret: false },
|
|
174
|
+
"build.repository.uri": { Value: `https://github.com/${repoFullName}`, IsSecret: false },
|
|
175
|
+
};
|
|
176
|
+
// Merge all step-level env: vars into the job Variables.
|
|
177
|
+
// The runner exports every Variable as a process env var for all steps, so this is the
|
|
178
|
+
// reliable mechanism to get DB_HOST=mysql, DB_PORT=3306 etc. into the step subprocess.
|
|
179
|
+
// (Step-scoped env would require full template compilation; job-level Variables are sufficient
|
|
180
|
+
// for DB credentials / CI flags that are consistent across steps.)
|
|
181
|
+
for (const step of payload.steps || []) {
|
|
182
|
+
if (step.Env && typeof step.Env === "object") {
|
|
183
|
+
for (const [key, val] of Object.entries(step.Env)) {
|
|
184
|
+
Variables[key] = { Value: String(val), IsSecret: false };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const githubContext = {
|
|
189
|
+
repository: repoFullName,
|
|
190
|
+
actor: ownerName,
|
|
191
|
+
sha: payload.headSha && payload.headSha !== "HEAD"
|
|
192
|
+
? payload.headSha
|
|
193
|
+
: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
194
|
+
ref: "refs/heads/main",
|
|
195
|
+
server_url: "https://github.com",
|
|
196
|
+
api_url: `${baseUrl}/_apis`,
|
|
197
|
+
graphql_url: `${baseUrl}/_graphql`,
|
|
198
|
+
workspace: workspacePath,
|
|
199
|
+
action: "__run",
|
|
200
|
+
token: "fake-token",
|
|
201
|
+
job: "local-job",
|
|
202
|
+
};
|
|
203
|
+
if (payload.pull_request) {
|
|
204
|
+
githubContext.event = {
|
|
205
|
+
pull_request: payload.pull_request,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
githubContext.event = {
|
|
210
|
+
repository: {
|
|
211
|
+
full_name: repoFullName,
|
|
212
|
+
name: repoName,
|
|
213
|
+
owner: { login: ownerName },
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Collect env vars from all steps (job-level env context seen by the runner's expression engine).
|
|
218
|
+
// Step-level `env:` blocks in the workflow YAML need to be exposed via ContextData.env so the
|
|
219
|
+
// runner can evaluate them. We merge all step envs — slightly broader than per-step scoping but
|
|
220
|
+
// correct for typical use (DB_HOST, DB_PORT, CI flags etc.).
|
|
221
|
+
const mergedStepEnv = {};
|
|
222
|
+
for (const step of payload.steps || []) {
|
|
223
|
+
if (step.Env) {
|
|
224
|
+
Object.assign(mergedStepEnv, step.Env);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const ContextData = {
|
|
228
|
+
github: toContextData(githubContext),
|
|
229
|
+
steps: { t: 2, d: [] }, // Empty steps context (required by EvaluateStepIf)
|
|
230
|
+
needs: { t: 2, d: [] }, // Empty needs context
|
|
231
|
+
strategy: { t: 2, d: [] }, // Empty strategy context
|
|
232
|
+
matrix: { t: 2, d: [] }, // Empty matrix context
|
|
233
|
+
// env context: merged from all step-level env: blocks so the runner's expression engine
|
|
234
|
+
// can substitute ${{ env.DB_HOST }} etc. during step execution.
|
|
235
|
+
...(Object.keys(mergedStepEnv).length > 0 ? { env: toContextData(mergedStepEnv) } : {}),
|
|
236
|
+
};
|
|
237
|
+
const generatedJobId = crypto.randomUUID();
|
|
238
|
+
const mockToken = createMockJwt(planId, generatedJobId);
|
|
239
|
+
const jobRequest = {
|
|
240
|
+
MessageType: "PipelineAgentJobRequest",
|
|
241
|
+
Plan: {
|
|
242
|
+
PlanId: planId,
|
|
243
|
+
PlanType: "Action",
|
|
244
|
+
ScopeId: crypto.randomUUID(),
|
|
245
|
+
},
|
|
246
|
+
Timeline: {
|
|
247
|
+
Id: crypto.randomUUID(),
|
|
248
|
+
ChangeId: 1,
|
|
249
|
+
},
|
|
250
|
+
JobId: generatedJobId,
|
|
251
|
+
RequestId: parseInt(jobId) || 1,
|
|
252
|
+
JobDisplayName: payload.name || "local-job",
|
|
253
|
+
JobName: payload.name || "local-job",
|
|
254
|
+
Steps: mappedSteps,
|
|
255
|
+
Variables: Variables,
|
|
256
|
+
ContextData: ContextData,
|
|
257
|
+
Resources: {
|
|
258
|
+
Repositories: [
|
|
259
|
+
{
|
|
260
|
+
Alias: "self",
|
|
261
|
+
Id: "repo-1",
|
|
262
|
+
Type: "git",
|
|
263
|
+
Version: payload.headSha || "HEAD",
|
|
264
|
+
Url: `https://github.com/${repoFullName}`,
|
|
265
|
+
Properties: {
|
|
266
|
+
id: "repo-1",
|
|
267
|
+
name: repoName,
|
|
268
|
+
fullName: repoFullName, // Required by types
|
|
269
|
+
repoFullName: repoFullName, // camelCase
|
|
270
|
+
owner: ownerName,
|
|
271
|
+
defaultBranch: payload.repository?.default_branch || "main",
|
|
272
|
+
cloneUrl: `https://github.com/${repoFullName}.git`,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
Endpoints: [
|
|
277
|
+
{
|
|
278
|
+
Name: "SystemVssConnection",
|
|
279
|
+
Url: baseUrl,
|
|
280
|
+
Authorization: {
|
|
281
|
+
Parameters: {
|
|
282
|
+
AccessToken: mockToken,
|
|
283
|
+
},
|
|
284
|
+
Scheme: "OAuth",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
Workspace: {
|
|
290
|
+
Path: workspacePath,
|
|
291
|
+
},
|
|
292
|
+
SystemVssConnection: {
|
|
293
|
+
Url: baseUrl,
|
|
294
|
+
Authorization: {
|
|
295
|
+
Parameters: {
|
|
296
|
+
AccessToken: mockToken,
|
|
297
|
+
},
|
|
298
|
+
Scheme: "OAuth",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
Actions: [],
|
|
302
|
+
MaskHints: [],
|
|
303
|
+
// EnvironmentVariables is IList<TemplateToken> in the runner — each element is a MappingToken.
|
|
304
|
+
// The runner evaluates each MappingToken and merges into Global.EnvironmentVariables (last wins),
|
|
305
|
+
// which then populates ExpressionValues["env"] → subprocess env vars.
|
|
306
|
+
EnvironmentVariables: Object.keys(mergedStepEnv).length > 0 ? [toTemplateTokenMapping(mergedStepEnv)] : [],
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
MessageId: 1,
|
|
310
|
+
MessageType: "PipelineAgentJobRequest",
|
|
311
|
+
Body: JSON.stringify(jobRequest),
|
|
312
|
+
};
|
|
313
|
+
}
|