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.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { Polka } from "polka";
2
+ export declare function registerActionRoutes(app: Polka): void;