actionspack 0.0.0 → 0.1.1

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,1329 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify, styleText } from "node:util";
3
+ import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { Evaluator, Lexer, Parser, data } from "@actions/expressions";
6
+ import { Binary, ContextAccess, FunctionCall, Grouping, IndexAccess, Literal, Logical, Star, Unary } from "@actions/expressions/ast";
7
+ import { truthy } from "@actions/expressions/result";
8
+ import { parse, stringify } from "yaml";
9
+ import { createHash } from "node:crypto";
10
+ import process from "node:process";
11
+ import { readFileSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
13
+ import { ACTION_ROOT } from "@actions/workflow-parser/actions/action-constants";
14
+ import { JSONObjectReader } from "@actions/workflow-parser/templates/json-object-reader";
15
+ import { TemplateSchema } from "@actions/workflow-parser/templates/schema/index";
16
+ import { TemplateContext, TemplateValidationErrors } from "@actions/workflow-parser/templates/template-context";
17
+ import { readTemplate } from "@actions/workflow-parser/templates/template-reader";
18
+ import { isBasicExpression, isBoolean, isMapping, isNumber, isSequence, isString } from "@actions/workflow-parser/templates/tokens/type-guards";
19
+ import { NoOperationTraceWriter } from "@actions/workflow-parser/templates/trace-writer";
20
+ import { WORKFLOW_ROOT } from "@actions/workflow-parser/workflows/workflow-constants";
21
+ import { YamlObjectReader } from "@actions/workflow-parser/workflows/yaml-object-reader";
22
+ import { Buffer } from "node:buffer";
23
+ import { tmpdir } from "node:os";
24
+ //#region src/utils/expression.ts
25
+ const EXPRESSION_CONTEXTS = [
26
+ "env",
27
+ "github",
28
+ "inputs",
29
+ "job",
30
+ "matrix",
31
+ "needs",
32
+ "runner",
33
+ "secrets",
34
+ "steps",
35
+ "strategy",
36
+ "vars"
37
+ ];
38
+ function parseExpression(expression) {
39
+ return new Parser(new Lexer(expression).lex().tokens, EXPRESSION_CONTEXTS, []).parse();
40
+ }
41
+ function expressionBody(value) {
42
+ if (!value.startsWith("${{") || !value.endsWith("}}")) return;
43
+ return value.slice(3, -2).trim();
44
+ }
45
+ function staticExpression(expr, values = {}) {
46
+ const evaluated = evaluateStaticExpression(expr, values);
47
+ if (evaluated) return evaluated;
48
+ if (expr instanceof Logical) return simplifyLogicalExpression(expr, values, (item) => printExpression(item, values)).static;
49
+ }
50
+ function simplifyLogicalExpression(expr, values, printExpression) {
51
+ const op = expr.operator.lexeme;
52
+ const pending = [];
53
+ let lastStatic;
54
+ for (const arg of expr.args) {
55
+ const staticArg = staticExpression(arg, values);
56
+ const text = staticArg?.text ?? printExpression(arg);
57
+ if (!staticArg) {
58
+ pending.push(text);
59
+ continue;
60
+ }
61
+ lastStatic = staticArg;
62
+ if (pending.length === 0) {
63
+ if (op === "&&" && !staticArg.truthy) return {
64
+ static: staticArg,
65
+ text
66
+ };
67
+ if (op === "&&" && staticArg.truthy) continue;
68
+ if (op === "||" && staticArg.truthy) return {
69
+ static: staticArg,
70
+ text
71
+ };
72
+ if (op === "||" && !staticArg.truthy) continue;
73
+ }
74
+ pending.push(text);
75
+ }
76
+ if (pending.length === 0 && lastStatic) return {
77
+ static: lastStatic,
78
+ text: lastStatic.text
79
+ };
80
+ if (pending.length === 1) return { text: pending[0] };
81
+ return { text: pending.join(` ${op} `) };
82
+ }
83
+ function staticIfExpression(expression) {
84
+ try {
85
+ const value = staticExpression(parseExpression(expression));
86
+ if (value?.value === true) return true;
87
+ if (value?.value === false) return false;
88
+ if (value?.value === "") return "";
89
+ return;
90
+ } catch {
91
+ return;
92
+ }
93
+ }
94
+ function printExpression(expr, values) {
95
+ const staticValue = staticExpression(expr, values);
96
+ if (staticValue) return staticValue.text;
97
+ if (expr instanceof Binary) return `${printExpression(expr.left, values)} ${expr.operator.lexeme} ${printExpression(expr.right, values)}`;
98
+ if (expr instanceof ContextAccess) return expr.name.lexeme;
99
+ if (expr instanceof FunctionCall) return printFunctionExpression(expr, values);
100
+ if (expr instanceof Grouping) return `(${printExpression(expr.group, values)})`;
101
+ if (expr instanceof IndexAccess) {
102
+ const replacement = replacementForIndexAccess(expr, values);
103
+ if (replacement !== void 0) return replacement;
104
+ return printIndexAccess(expr, values);
105
+ }
106
+ if (expr instanceof Literal) return expr.token.lexeme;
107
+ if (expr instanceof Logical) return simplifyLogicalExpression(expr, values, (item) => printExpression(item, values)).text;
108
+ if (expr instanceof Star) return "*";
109
+ if (expr instanceof Unary) return `${expr.operator.lexeme}${printExpression(expr.expr, values)}`;
110
+ throw new Error("Unsupported expression node");
111
+ }
112
+ function literalString(expr) {
113
+ if (typeof expr.token.value === "string") return expr.token.value;
114
+ const literal = expr.literal;
115
+ return typeof literal.value === "string" ? literal.value : "";
116
+ }
117
+ function valueForIndexAccess(expr, values) {
118
+ if (!(expr instanceof IndexAccess)) return;
119
+ return hasValueForIndexAccess(expr, values) ? values[indexAccessKey(expr)] : void 0;
120
+ }
121
+ function quoteExpressionString(value) {
122
+ return `'${value.replaceAll("'", "''")}'`;
123
+ }
124
+ function valueLiteral(value) {
125
+ if (value === void 0 || value === null) return "null";
126
+ if (typeof value === "boolean") return value ? "true" : "false";
127
+ if (typeof value === "number") return String(value);
128
+ if (typeof value === "string") return quoteExpressionString(value);
129
+ return `fromJson(${quoteExpressionString(JSON.stringify(value))})`;
130
+ }
131
+ function evaluateStaticExpression(expr, values) {
132
+ if (!canEvaluateExpression(expr, values)) return;
133
+ try {
134
+ const data = new Evaluator(expr, expressionContext(values)).evaluate();
135
+ return {
136
+ data,
137
+ text: dataLiteral(data),
138
+ truthy: truthy(data),
139
+ value: dataValue(data)
140
+ };
141
+ } catch {
142
+ return;
143
+ }
144
+ }
145
+ function hasValueForIndexAccess(expr, values) {
146
+ const key = indexAccessKey(expr);
147
+ return key !== void 0 && Object.hasOwn(values, key);
148
+ }
149
+ function indexAccessKey(expr) {
150
+ if (!(expr.expr instanceof ContextAccess) || !(expr.index instanceof Literal)) return;
151
+ const root = expr.expr.name.lexeme;
152
+ if (root !== "inputs" && root !== "secrets") return;
153
+ const name = literalString(expr.index);
154
+ return name ? `${root}.${name}` : void 0;
155
+ }
156
+ function canEvaluateExpression(expr, values) {
157
+ if (expr instanceof Binary) return canEvaluateExpression(expr.left, values) && canEvaluateExpression(expr.right, values);
158
+ if (expr instanceof ContextAccess) return Object.hasOwn(values, expr.name.lexeme);
159
+ if (expr instanceof FunctionCall) return expr.args.every((arg) => canEvaluateExpression(arg, values));
160
+ if (expr instanceof Grouping) return canEvaluateExpression(expr.group, values);
161
+ if (expr instanceof IndexAccess) return hasStaticValueForIndexAccess(expr, values) || canEvaluateExpression(expr.expr, values) && canEvaluateExpression(expr.index, values);
162
+ if (expr instanceof Literal) return true;
163
+ if (expr instanceof Logical) return expr.args.every((arg) => canEvaluateExpression(arg, values));
164
+ if (expr instanceof Star) return false;
165
+ if (expr instanceof Unary) return canEvaluateExpression(expr.expr, values);
166
+ return false;
167
+ }
168
+ function expressionContext(values) {
169
+ const roots = /* @__PURE__ */ new Map();
170
+ const pairs = [];
171
+ for (const [key, value] of Object.entries(values)) {
172
+ const [root, ...rest] = key.split(".");
173
+ if (!root) continue;
174
+ if (rest.length === 0) {
175
+ pairs.push({
176
+ key: root,
177
+ value: toExpressionData(value)
178
+ });
179
+ continue;
180
+ }
181
+ const name = rest.join(".");
182
+ const entries = roots.get(root) ?? /* @__PURE__ */ new Map();
183
+ entries.set(name, value);
184
+ roots.set(root, entries);
185
+ }
186
+ for (const [root, entries] of roots) pairs.push({
187
+ key: root,
188
+ value: new data.Dictionary(...[...entries].map(([key, value]) => ({
189
+ key,
190
+ value: toExpressionData(value)
191
+ })))
192
+ });
193
+ return new data.Dictionary(...pairs);
194
+ }
195
+ function toExpressionData(value) {
196
+ if (value === void 0 || value === null) return new data.Null();
197
+ if (typeof value === "boolean") return new data.BooleanData(value);
198
+ if (typeof value === "number") return new data.NumberData(value);
199
+ if (typeof value === "string") return new data.StringData(value);
200
+ if (Array.isArray(value)) return new data.Array(...value.map((item) => toExpressionData(item)));
201
+ if (typeof value === "object") return new data.Dictionary(...Object.entries(value).map(([key, item]) => ({
202
+ key,
203
+ value: toExpressionData(item)
204
+ })));
205
+ return new data.Null();
206
+ }
207
+ function dataValue(value) {
208
+ if (value instanceof data.Array) return value.values().map((item) => dataValue(item));
209
+ if (value instanceof data.BooleanData) return value.value;
210
+ if (value instanceof data.Dictionary) return Object.fromEntries(value.pairs().map((pair) => [pair.key, dataValue(pair.value)]));
211
+ if (value instanceof data.Null) return null;
212
+ if (value instanceof data.NumberData) return value.value;
213
+ if (value instanceof data.StringData) return value.value;
214
+ return null;
215
+ }
216
+ function dataLiteral(value) {
217
+ return valueLiteral(dataValue(value));
218
+ }
219
+ function replacementForIndexAccess(expr, values) {
220
+ if (!hasValueForIndexAccess(expr, values)) return;
221
+ const value = valueForIndexAccess(expr, values);
222
+ if (typeof value === "string") {
223
+ const body = expressionBody(value.trim());
224
+ if (body !== void 0) return `(${body})`;
225
+ }
226
+ return valueLiteral(value);
227
+ }
228
+ function hasStaticValueForIndexAccess(expr, values) {
229
+ if (!hasValueForIndexAccess(expr, values)) return false;
230
+ const value = valueForIndexAccess(expr, values);
231
+ return typeof value !== "string" || expressionBody(value.trim()) === void 0;
232
+ }
233
+ function printIndexAccess(expr, values) {
234
+ const target = printExpression(expr.expr, values);
235
+ if (expr.index instanceof Literal) return expr.index.token.value === void 0 ? `${target}.${expr.index.token.lexeme}` : `${target}[${expr.index.token.lexeme}]`;
236
+ return `${target}[${printExpression(expr.index, values)}]`;
237
+ }
238
+ function printFunctionExpression(expr, values) {
239
+ const format = printFormatExpression(expr, values);
240
+ if (format) return format;
241
+ return `${expr.functionName.lexeme}(${expr.args.map((arg) => printExpression(arg, values)).join(", ")})`;
242
+ }
243
+ function printFormatExpression(expr, values) {
244
+ if (expr.functionName.lexeme.toLowerCase() !== "format") return;
245
+ const [formatArg, ...args] = expr.args;
246
+ if (!formatArg) return;
247
+ const format = staticExpression(formatArg, values);
248
+ if (typeof format?.value !== "string") return;
249
+ const simplified = simplifyFormatString(format.value, args.map((arg) => ({
250
+ static: staticExpression(arg, values),
251
+ text: printExpression(arg, values)
252
+ })));
253
+ if (!simplified) return;
254
+ if (simplified.args.length === 0) return quoteExpressionString(simplified.format);
255
+ return `format(${quoteExpressionString(simplified.format)}, ${simplified.args.join(", ")})`;
256
+ }
257
+ function simplifyFormatString(format, args) {
258
+ const nextArgs = [];
259
+ let nextFormat = "";
260
+ let index = 0;
261
+ while (index < format.length) {
262
+ const char = format[index];
263
+ const nextChar = format[index + 1];
264
+ if (char === "{" && nextChar === "{") {
265
+ nextFormat += "{{";
266
+ index += 2;
267
+ continue;
268
+ }
269
+ if (char === "}" && nextChar === "}") {
270
+ nextFormat += "}}";
271
+ index += 2;
272
+ continue;
273
+ }
274
+ if (char !== "{") {
275
+ nextFormat += char;
276
+ index += 1;
277
+ continue;
278
+ }
279
+ const end = format.indexOf("}", index + 1);
280
+ if (end === -1) return;
281
+ const rawIndex = /^\d+/u.exec(format.slice(index + 1, end))?.[0];
282
+ if (!rawIndex) return;
283
+ const arg = args[Number(rawIndex)];
284
+ if (!arg) return;
285
+ if (arg.static) nextFormat += escapeFormatLiteral(arg.static.data.coerceString());
286
+ else {
287
+ const nextIndex = nextArgs.push(arg.text) - 1;
288
+ nextFormat += `{${nextIndex}}`;
289
+ }
290
+ index = end + 1;
291
+ }
292
+ nextFormat = nextFormat.trim();
293
+ return {
294
+ args: nextArgs,
295
+ format: nextFormat
296
+ };
297
+ }
298
+ function escapeFormatLiteral(value) {
299
+ return value.replaceAll("{", "{{").replaceAll("}", "}}");
300
+ }
301
+ //#endregion
302
+ //#region src/utils/yaml.ts
303
+ function parseYamlMap(source, file) {
304
+ const value = parse(source);
305
+ if (!isRecord(value)) throw new TypeError(`${file} must contain a YAML mapping`);
306
+ return value;
307
+ }
308
+ function stringifyYaml(value) {
309
+ return stringify(value, {
310
+ lineWidth: 0,
311
+ minContentWidth: 0,
312
+ singleQuote: true
313
+ });
314
+ }
315
+ function stringifyWorkflowYaml(value) {
316
+ return formatWorkflowYaml(stringifyYaml(normalizeWorkflowYamlValue(value)));
317
+ }
318
+ function isRecord(value) {
319
+ return typeof value === "object" && value !== null && !Array.isArray(value);
320
+ }
321
+ function asRecord(value) {
322
+ return isRecord(value) ? value : void 0;
323
+ }
324
+ function asArray(value) {
325
+ return Array.isArray(value) ? value : [];
326
+ }
327
+ function formatWorkflowYaml(source) {
328
+ const lines = source.trimEnd().split("\n");
329
+ const output = [];
330
+ let stepsIndent;
331
+ let stepItemIndent;
332
+ for (const line of lines) {
333
+ const indent = leadingSpaces(line);
334
+ const trimmed = line.trim();
335
+ const isTopLevelKey = indent === 0 && /^[\w-]+:/u.test(line);
336
+ if (stepsIndent !== void 0 && trimmed && indent <= stepsIndent) {
337
+ pushBlankLine(output);
338
+ stepsIndent = void 0;
339
+ stepItemIndent = void 0;
340
+ }
341
+ if (isTopLevelKey && output.length > 0) pushBlankLine(output);
342
+ if (stepsIndent !== void 0 && stepItemIndent !== void 0 && indent === stepItemIndent && trimmed.startsWith("- ")) pushBlankLine(output);
343
+ output.push(line);
344
+ if (/^steps:\s*$/u.test(trimmed)) {
345
+ stepsIndent = indent;
346
+ stepItemIndent = void 0;
347
+ continue;
348
+ }
349
+ if (stepsIndent !== void 0 && indent > stepsIndent && trimmed.startsWith("- ")) stepItemIndent ??= indent;
350
+ }
351
+ return `${output.join("\n")}\n`;
352
+ }
353
+ function leadingSpaces(value) {
354
+ return value.length - value.trimStart().length;
355
+ }
356
+ function pushBlankLine(lines) {
357
+ if (lines.length > 0 && lines.at(-1) !== "") lines.push("");
358
+ }
359
+ function normalizeWorkflowYamlValue(value) {
360
+ if (Array.isArray(value)) return value.map((item) => normalizeWorkflowYamlValue(item));
361
+ if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeWorkflowYamlValue(item)]));
362
+ if (value === "true") return true;
363
+ if (value === "false") return false;
364
+ return value;
365
+ }
366
+ //#endregion
367
+ //#region src/optimizer.ts
368
+ function optimizeJob(job) {
369
+ const next = { ...job };
370
+ if (isEmptyNeeds(next.needs)) delete next.needs;
371
+ if (staticIfValue(next.if) === true) delete next.if;
372
+ return next;
373
+ }
374
+ function optimizeStep(stepValue) {
375
+ const step = asRecord(stepValue);
376
+ if (!step) return stepValue;
377
+ const ifValue = staticIfValue(step.if);
378
+ if (ifValue === false || ifValue === "") return;
379
+ if (ifValue === true) {
380
+ const next = { ...step };
381
+ delete next.if;
382
+ return next;
383
+ }
384
+ return stepValue;
385
+ }
386
+ function optimizeJobs(jobs) {
387
+ return Object.fromEntries(Object.entries(jobs).map(([jobId, jobValue]) => {
388
+ const job = asRecord(jobValue);
389
+ return [jobId, job ? optimizeJob(job) : jobValue];
390
+ }));
391
+ }
392
+ function staticIfValue(value) {
393
+ if (value === true || value === false || value === "") return value;
394
+ if (typeof value !== "string") return;
395
+ const trimmed = value.trim();
396
+ return staticIfExpression(expressionBody(trimmed) ?? trimmed);
397
+ }
398
+ function isEmptyNeeds(value) {
399
+ return Array.isArray(value) && value.length === 0;
400
+ }
401
+ //#endregion
402
+ //#region src/utils/workflow-parser.ts
403
+ const workflowParserEntry = fileURLToPath(import.meta.resolve("@actions/workflow-parser"));
404
+ const workflowParserDist = path.dirname(workflowParserEntry);
405
+ let actionSchema;
406
+ let workflowSchema;
407
+ function parseWorkflowMap(source, file) {
408
+ return parseTemplateMap(WORKFLOW_ROOT, workflowSchemaForParser(), source, file);
409
+ }
410
+ function parseActionMap(source, file) {
411
+ return parseTemplateMap(ACTION_ROOT, actionSchemaForParser(), source, file);
412
+ }
413
+ function parseTemplateMap(root, schema, source, file) {
414
+ const result = parseTemplate(root, schema, source, file);
415
+ throwOnTemplateErrors(result, file);
416
+ if (!result.value) return {};
417
+ const value = templateTokenToValue(result.value);
418
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${file} must be a YAML mapping`);
419
+ return value;
420
+ }
421
+ function parseActionTemplate(source, file) {
422
+ return parseTemplate(ACTION_ROOT, actionSchemaForParser(), source, file);
423
+ }
424
+ function parseTemplate(root, schema, source, file) {
425
+ const context = new TemplateContext(new TemplateValidationErrors(), schema, new NoOperationTraceWriter());
426
+ const fileId = context.getFileId(file);
427
+ const reader = new YamlObjectReader(fileId, source);
428
+ if (reader.errors.length > 0) {
429
+ for (const error of reader.errors) context.error(fileId, error.message, error.range);
430
+ return {
431
+ context,
432
+ value: void 0
433
+ };
434
+ }
435
+ return {
436
+ context,
437
+ value: readTemplate(context, root, reader, fileId)
438
+ };
439
+ }
440
+ function throwOnTemplateErrors(result, file) {
441
+ const errors = result.context.errors.getErrors();
442
+ if (errors.length > 0) throw new Error(`${file}: ${errors.map((error) => error.message).join("\n")}`);
443
+ }
444
+ function templateTokenToValue(token) {
445
+ if (isMapping(token)) return Object.fromEntries([...token].map((pair) => [String(templateTokenToValue(pair.key)), templateTokenToValue(pair.value)]));
446
+ if (isSequence(token)) return [...token].map((item) => templateTokenToValue(item));
447
+ if (isString(token) || isNumber(token) || isBoolean(token)) return token.value;
448
+ if (isBasicExpression(token)) return token.toString();
449
+ return token.toJSON();
450
+ }
451
+ function workflowSchemaForParser() {
452
+ workflowSchema ??= loadSchema("workflow-v1.0.min.json");
453
+ return workflowSchema;
454
+ }
455
+ function actionSchemaForParser() {
456
+ actionSchema ??= loadSchema("action-v1.0.min.json");
457
+ return actionSchema;
458
+ }
459
+ function loadSchema(name) {
460
+ const file = path.join(workflowParserDist, name);
461
+ return TemplateSchema.load(new JSONObjectReader(void 0, readFileSync(file, "utf8")));
462
+ }
463
+ //#endregion
464
+ //#region src/utils/fs.ts
465
+ const LOCKFILE_PATH = ".github/workflow.lock.yml";
466
+ const DEFAULT_SOURCE_DIR = ".github/workflows/src";
467
+ const DEFAULT_OUTPUT_DIR = ".github/workflows";
468
+ function resolveCwd(cwd) {
469
+ return cwd ? path.resolve(cwd) : process.cwd();
470
+ }
471
+ async function fileExists(file) {
472
+ try {
473
+ await access(file);
474
+ return true;
475
+ } catch {
476
+ return false;
477
+ }
478
+ }
479
+ async function readYamlFile(file) {
480
+ return parseYamlMap(await readFile(file, "utf8"), file);
481
+ }
482
+ async function readWorkflowFile(file) {
483
+ return parseWorkflowMap(await readFile(file, "utf8"), file);
484
+ }
485
+ async function writeYamlFile(file, value) {
486
+ await mkdir(path.dirname(file), { recursive: true });
487
+ await writeFile(file, stringifyYaml(value), "utf8");
488
+ }
489
+ async function discoverConfig(cwd, overrides = {}) {
490
+ const root = resolveCwd(cwd);
491
+ const configPath = path.join(root, "actionspack.yml");
492
+ let config;
493
+ if (await fileExists(configPath)) {
494
+ const rawConfig = await readYamlFile(configPath);
495
+ config = {
496
+ entries: (Array.isArray(rawConfig.entries) ? rawConfig.entries : []).map((entry) => {
497
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new TypeError("actionspack.yml entries must be mappings");
498
+ const source = Reflect.get(entry, "source");
499
+ const output = Reflect.get(entry, "output");
500
+ if (typeof source !== "string" || typeof output !== "string") throw new TypeError("actionspack.yml entries require source and output");
501
+ return {
502
+ source,
503
+ output
504
+ };
505
+ }),
506
+ external: normalizeStringList(rawConfig.external)
507
+ };
508
+ } else config = await discoverDefaultConfig(root);
509
+ return {
510
+ ...config,
511
+ ...normalizeConfigOverrides(overrides)
512
+ };
513
+ }
514
+ async function discoverDefaultConfig(root) {
515
+ const sourceDir = path.join(root, DEFAULT_SOURCE_DIR);
516
+ if (!await fileExists(sourceDir)) return {
517
+ entries: [],
518
+ external: []
519
+ };
520
+ return {
521
+ external: [],
522
+ entries: (await readdir(sourceDir)).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).toSorted().map((name) => ({
523
+ source: path.posix.join(DEFAULT_SOURCE_DIR, name),
524
+ output: path.posix.join(DEFAULT_OUTPUT_DIR, name.replace(/\.ya?ml$/u, ".yml"))
525
+ }))
526
+ };
527
+ }
528
+ function normalizeConfigOverrides(overrides) {
529
+ return {
530
+ ...overrides.entries ? { entries: overrides.entries } : {},
531
+ ...overrides.external ? { external: overrides.external } : {}
532
+ };
533
+ }
534
+ function normalizeStringList(value) {
535
+ if (typeof value === "string") return [value];
536
+ if (!Array.isArray(value)) return [];
537
+ return value.filter((item) => typeof item === "string");
538
+ }
539
+ function readWorkflowEntry(root, entry) {
540
+ return readWorkflowFile(path.join(root, entry.source));
541
+ }
542
+ function emptyLockfile() {
543
+ return {
544
+ lockfileVersion: 1,
545
+ entries: {},
546
+ packages: {}
547
+ };
548
+ }
549
+ async function readLockfile(cwd) {
550
+ const root = resolveCwd(cwd);
551
+ const file = path.join(root, LOCKFILE_PATH);
552
+ if (!await fileExists(file)) return emptyLockfile();
553
+ const value = await readYamlFile(file);
554
+ return {
555
+ lockfileVersion: 1,
556
+ entries: normalizeRecord(value.entries),
557
+ packages: normalizeRecord(value.packages)
558
+ };
559
+ }
560
+ async function writeLockfile(cwd, lockfile) {
561
+ await writeYamlFile(path.join(resolveCwd(cwd), LOCKFILE_PATH), lockfile);
562
+ }
563
+ async function writeWorkflow(cwd, output, workflow) {
564
+ const file = path.join(resolveCwd(cwd), output);
565
+ await mkdir(path.dirname(file), { recursive: true });
566
+ await writeFile(file, stringifyWorkflowYaml(workflow), "utf8");
567
+ }
568
+ function normalizeRecord(value) {
569
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
570
+ return value;
571
+ }
572
+ //#endregion
573
+ //#region src/utils/github.ts
574
+ var HttpGitHubClient = class {
575
+ #cacheDir;
576
+ #token;
577
+ constructor(token = process.env.GITHUB_TOKEN, cacheDir = path.join(tmpdir(), "actionspack-github-cache")) {
578
+ this.#cacheDir = cacheDir;
579
+ this.#token = token;
580
+ }
581
+ async resolveRef(owner, repo, ref) {
582
+ if (/^[a-f0-9]{40}$/iu.test(ref)) return ref;
583
+ const candidates = [
584
+ `heads/${ref}`,
585
+ `tags/${ref}`,
586
+ ref
587
+ ];
588
+ for (const candidate of candidates) {
589
+ const sha = (await this.#request(`/repos/${owner}/${repo}/git/ref/${candidate}`))?.object?.sha;
590
+ if (sha) return sha;
591
+ }
592
+ throw new Error(`Unable to resolve ${owner}/${repo}@${ref}`);
593
+ }
594
+ async readFile(owner, repo, ref, filePath) {
595
+ const cacheFile = this.#cacheFile(owner, repo, ref, filePath);
596
+ const cached = await readFile(cacheFile, "utf8").catch(() => void 0);
597
+ if (cached !== void 0) return cached;
598
+ const response = await this.#request(`/repos/${owner}/${repo}/contents/${encodeURIComponentPath(filePath)}?ref=${ref}`, true);
599
+ if (!response) return;
600
+ if (response.encoding !== "base64" || typeof response.content !== "string") throw new Error(`Unexpected GitHub content response for ${owner}/${repo}/${filePath}@${ref}`);
601
+ const content = Buffer.from(response.content, "base64").toString("utf8");
602
+ await mkdir(path.dirname(cacheFile), { recursive: true });
603
+ await writeFile(cacheFile, content, "utf8");
604
+ return content;
605
+ }
606
+ #cacheFile(owner, repo, ref, filePath) {
607
+ return path.join(this.#cacheDir, owner, repo, ref, ...filePath.split("/"));
608
+ }
609
+ async #request(pathname, allowNotFound = false) {
610
+ const headers = {
611
+ accept: "application/vnd.github+json",
612
+ "user-agent": "actionspack",
613
+ "x-github-api-version": "2022-11-28"
614
+ };
615
+ if (this.#token) headers.authorization = `Bearer ${this.#token}`;
616
+ const response = await fetch(`https://api.github.com${pathname}`, { headers });
617
+ if (response.status === 404 && allowNotFound) return;
618
+ if (!response.ok) throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`);
619
+ return await response.json();
620
+ }
621
+ };
622
+ function encodeURIComponentPath(filePath) {
623
+ return filePath.split("/").map(encodeURIComponent).join("/");
624
+ }
625
+ //#endregion
626
+ //#region src/utils/ref.ts
627
+ const REMOTE_USES_RE = /^[\w.-]+\/[\w.-]+(?:\/[^@\s]+)?@[^@\s]+$/;
628
+ function packageKey(owner, repo, path) {
629
+ return path && path !== "." ? `github:${owner}/${repo}/${path}` : `github:${owner}/${repo}`;
630
+ }
631
+ function isRemoteUses(value) {
632
+ return typeof value === "string" && REMOTE_USES_RE.test(value);
633
+ }
634
+ function isPinnedRemoteUses(value) {
635
+ return isRemoteUses(value) && /@[a-f0-9]{40}$/iu.test(value);
636
+ }
637
+ function parseRemoteUses(value, kind = "action") {
638
+ const atIndex = value.lastIndexOf("@");
639
+ if (atIndex <= 0 || atIndex === value.length - 1) throw new Error(`Malformed remote reference: ${value}`);
640
+ const ref = value.slice(atIndex + 1);
641
+ const parts = value.slice(0, atIndex).split("/");
642
+ if (parts.length < 2 || !parts[0] || !parts[1]) throw new Error(`Malformed remote reference: ${value}`);
643
+ const owner = parts[0];
644
+ const repo = parts[1];
645
+ const path = parts.length > 2 ? parts.slice(2).join("/") : ".";
646
+ const inferredKind = path.startsWith(".github/workflows/") ? "reusable-workflow" : kind;
647
+ return {
648
+ owner,
649
+ repo,
650
+ path,
651
+ ref,
652
+ package: packageKey(owner, repo, path),
653
+ kind: inferredKind
654
+ };
655
+ }
656
+ function matchesPackageSelector(key, selector) {
657
+ const normalized = selector.startsWith("github:") ? selector : `github:${selector}`;
658
+ return key === normalized || key.startsWith(`${normalized}/`) || key.includes(`:${selector}/`);
659
+ }
660
+ function matchesExternalSelector(remote, selector) {
661
+ const value = selector.replace(/^github:/u, "");
662
+ const fullName = `${remote.owner}/${remote.repo}`;
663
+ const withPath = `${fullName}/${remote.path}`;
664
+ const packageSelector = `github:${value}`;
665
+ return remote.package === selector || remote.package === packageSelector || fullName === value || withPath === value || remote.package.startsWith(`${packageSelector}/`);
666
+ }
667
+ function pinnedUses(remote, resolved) {
668
+ return `${remote.path === "." ? `${remote.owner}/${remote.repo}` : `${remote.owner}/${remote.repo}/${remote.path}`}@${resolved}`;
669
+ }
670
+ //#endregion
671
+ //#region src/scan.ts
672
+ async function scan(options = {}) {
673
+ const cwd = resolveCwd(options.cwd);
674
+ const config = await discoverConfig(cwd, options);
675
+ const previous = await readLockfile(cwd);
676
+ const github = options.github ?? new HttpGitHubClient();
677
+ const lockfile = {
678
+ lockfileVersion: 1,
679
+ entries: {},
680
+ packages: { ...previous.packages }
681
+ };
682
+ const queue = [];
683
+ for (const entry of config.entries) {
684
+ const dependencies = collectWorkflowDependencies(await readWorkflowEntry(cwd, entry), entry.source);
685
+ lockfile.entries[entry.source] = {
686
+ output: entry.output,
687
+ dependencies: dependencies.map(toLockDependency)
688
+ };
689
+ queue.push(...dependencies);
690
+ }
691
+ await resolveQueue(lockfile, queue, previous, github, options.refreshPackages ?? /* @__PURE__ */ new Set(), {
692
+ external: config.external,
693
+ stderr: options.stderr,
694
+ stdout: options.stdout
695
+ });
696
+ lockfile.packages = prunePackages(lockfile);
697
+ await writeLockfile(cwd, lockfile);
698
+ return {
699
+ lockfile,
700
+ entries: config.entries
701
+ };
702
+ }
703
+ function collectWorkflowDependencies(workflow, source) {
704
+ const dependencies = [];
705
+ const jobs = asRecord(workflow.jobs);
706
+ if (!jobs) return dependencies;
707
+ for (const [jobId, jobValue] of Object.entries(jobs)) {
708
+ const job = asRecord(jobValue);
709
+ if (!job) continue;
710
+ if (typeof job.uses === "string") {
711
+ if (isRemoteUses(job.uses)) dependencies.push(toDependency(job.uses, "reusable-workflow", `${source}#jobs.${jobId}.uses`));
712
+ continue;
713
+ }
714
+ dependencies.push(...collectStepDependencies(asArray(job.steps), `${source}#jobs.${jobId}.steps`));
715
+ }
716
+ return uniqueDependencies(dependencies);
717
+ }
718
+ function collectStepDependencies(steps, foundAtPrefix) {
719
+ const dependencies = [];
720
+ for (const [index, stepValue] of steps.entries()) {
721
+ const step = asRecord(stepValue);
722
+ if (!step || typeof step.uses !== "string") continue;
723
+ if (isRemoteUses(step.uses)) dependencies.push(toDependency(step.uses, "action", `${foundAtPrefix}[${index}].uses`));
724
+ }
725
+ return uniqueDependencies(dependencies);
726
+ }
727
+ async function resolveQueue(lockfile, queue, previous, github, refreshPackages, context) {
728
+ const seen = /* @__PURE__ */ new Set();
729
+ const resolvedRefs = /* @__PURE__ */ new Map();
730
+ const warnings = /* @__PURE__ */ new Set();
731
+ while (queue.length > 0) {
732
+ const dependency = queue.shift();
733
+ const shouldRefresh = refreshPackages.has(dependency.package);
734
+ const previousPackage = previous.packages[dependency.package];
735
+ const resolved = previousPackage?.resolved && !shouldRefresh ? previousPackage.resolved : await resolveDependencyRef(dependency, github, context, resolvedRefs);
736
+ const seenKey = `${dependency.package}@${resolved}`;
737
+ if (seen.has(seenKey)) continue;
738
+ const scanned = await scanRemotePackage(dependency.remote, resolved, github, context, warnings);
739
+ lockfile.packages[dependency.package] = {
740
+ ...scanned,
741
+ requested: dependency.requested,
742
+ resolved
743
+ };
744
+ seen.add(seenKey);
745
+ queue.push(...scanned.dependencies.map((item) => ({
746
+ ...item,
747
+ remote: parseRemoteUsesFromDependency(item)
748
+ })));
749
+ }
750
+ }
751
+ async function scanRemotePackage(remote, resolved, github, context, warnings) {
752
+ if (remote.kind === "reusable-workflow") {
753
+ if (isExternal(remote, context.external)) {
754
+ context.stdout?.write(`External ${remote.owner}/${remote.repo}/${remote.path}@${resolved}\n`);
755
+ return externalPackage(remote, "external-workflow");
756
+ }
757
+ const content = await github.readFile(remote.owner, remote.repo, resolved, remote.path);
758
+ if (!content) throw new Error(`Missing reusable workflow ${remote.owner}/${remote.repo}/${remote.path}@${resolved}`);
759
+ const dependencies = collectWorkflowDependencies(parseWorkflowMap(content, remote.path), `${remote.owner}/${remote.repo}/${remote.path}`);
760
+ return {
761
+ source: "github",
762
+ owner: remote.owner,
763
+ repo: remote.repo,
764
+ path: remote.path,
765
+ type: "reusable-workflow",
766
+ contentDigest: digest(content),
767
+ dependencies: dependencies.map(toLockDependency)
768
+ };
769
+ }
770
+ if (isExternal(remote, context.external)) {
771
+ context.stdout?.write(`External ${remote.owner}/${remote.repo}/${remote.path}@${resolved}\n`);
772
+ return externalPackage(remote, "external-action");
773
+ }
774
+ const metadata = await readActionMetadata(github, remote, resolved);
775
+ const runs = asRecord(parseActionMap(metadata.content, `${remote.path}/action.yml`).runs);
776
+ const using = typeof runs?.using === "string" ? runs.using.toLowerCase() : void 0;
777
+ if (using !== "composite") {
778
+ warnOnce(context, warnings, `Unsupported action type for ${remote.owner}/${remote.repo}/${remote.path}@${resolved}: ${using ?? "unknown"}; marking external`);
779
+ return {
780
+ ...externalPackage(remote, "external-action"),
781
+ contentDigest: digest(metadata.content)
782
+ };
783
+ }
784
+ const dependencies = collectStepDependencies(asArray(runs?.steps), `${remote.owner}/${remote.repo}/${remote.path}#runs.steps`);
785
+ return {
786
+ source: "github",
787
+ owner: remote.owner,
788
+ repo: remote.repo,
789
+ path: remote.path,
790
+ type: "composite",
791
+ contentDigest: digest(metadata.content),
792
+ dependencies: dependencies.map(toLockDependency)
793
+ };
794
+ }
795
+ function resolveDependencyRef(dependency, github, context, resolvedRefs) {
796
+ const key = `${dependency.package}@${dependency.requested}`;
797
+ const resolved = resolvedRefs.get(key);
798
+ if (resolved) return Promise.resolve(resolved);
799
+ context.stdout?.write(`Resolving ${dependency.remote.owner}/${dependency.remote.repo}${dependency.remote.path === "." ? "" : `/${dependency.remote.path}`}@${dependency.requested}\n`);
800
+ return github.resolveRef(dependency.remote.owner, dependency.remote.repo, dependency.requested).then((value) => {
801
+ resolvedRefs.set(key, value);
802
+ return value;
803
+ });
804
+ }
805
+ function warnOnce(context, warnings, message) {
806
+ if (warnings.has(message)) return;
807
+ warnings.add(message);
808
+ context.stderr?.write(`${styleText("yellow", `Warning: ${message}`)}\n`);
809
+ }
810
+ function isExternal(remote, selectors) {
811
+ return selectors.some((selector) => matchesExternalSelector(remote, selector));
812
+ }
813
+ function externalPackage(remote, type) {
814
+ return {
815
+ source: "github",
816
+ owner: remote.owner,
817
+ repo: remote.repo,
818
+ path: remote.path,
819
+ type,
820
+ external: true,
821
+ contentDigest: "external",
822
+ dependencies: []
823
+ };
824
+ }
825
+ async function readActionMetadata(github, remote, resolved) {
826
+ const base = remote.path === "." ? "" : `${remote.path.replace(/\/$/u, "")}/`;
827
+ for (const name of ["action.yml", "action.yaml"]) {
828
+ const metadataPath = `${base}${name}`;
829
+ const content = await github.readFile(remote.owner, remote.repo, resolved, metadataPath);
830
+ if (content) return {
831
+ path: metadataPath,
832
+ content
833
+ };
834
+ }
835
+ throw new Error(`Missing action metadata for ${remote.owner}/${remote.repo}/${remote.path}@${resolved}`);
836
+ }
837
+ function parseRemoteUsesFromDependency(dependency) {
838
+ const match = /^github:([^/]+)\/([^/]+)(?:\/(.+))?$/u.exec(dependency.package);
839
+ if (!match) throw new Error(`Unsupported package key: ${dependency.package}`);
840
+ const [, owner, repo, packagePath] = match;
841
+ const remotePath = packagePath ?? ".";
842
+ const kind = remotePath.startsWith(".github/workflows/") ? "reusable-workflow" : "action";
843
+ return {
844
+ owner,
845
+ repo,
846
+ path: remotePath,
847
+ ref: dependency.requested,
848
+ package: dependency.package,
849
+ kind
850
+ };
851
+ }
852
+ function toDependency(value, kind, foundAt) {
853
+ const remote = parseRemoteUses(value, kind);
854
+ return {
855
+ package: remote.package,
856
+ requested: remote.ref,
857
+ foundAt,
858
+ remote
859
+ };
860
+ }
861
+ function toLockDependency(dependency) {
862
+ return {
863
+ package: dependency.package,
864
+ requested: dependency.requested,
865
+ ...dependency.resolved ? { resolved: dependency.resolved } : {},
866
+ ...dependency.foundAt ? { foundAt: dependency.foundAt } : {}
867
+ };
868
+ }
869
+ function uniqueDependencies(dependencies) {
870
+ const seen = /* @__PURE__ */ new Set();
871
+ return dependencies.filter((dependency) => {
872
+ const key = `${dependency.package}@${dependency.requested}`;
873
+ if (seen.has(key)) return false;
874
+ seen.add(key);
875
+ return true;
876
+ });
877
+ }
878
+ function prunePackages(lockfile) {
879
+ const reachable = /* @__PURE__ */ new Set();
880
+ const visit = (dependency) => {
881
+ if (reachable.has(dependency.package)) return;
882
+ const item = lockfile.packages[dependency.package];
883
+ if (!item) return;
884
+ reachable.add(dependency.package);
885
+ item.dependencies.forEach(visit);
886
+ };
887
+ for (const entry of Object.values(lockfile.entries)) entry.dependencies.forEach(visit);
888
+ return Object.fromEntries([...reachable].toSorted().map((key) => [key, lockfile.packages[key]]));
889
+ }
890
+ function digest(content) {
891
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
892
+ }
893
+ //#endregion
894
+ //#region src/utils/substitute.ts
895
+ function substituteValue(value, values, key) {
896
+ if (typeof value === "string") {
897
+ const next = substituteString(value, values);
898
+ return key === "run" && typeof next === "string" ? next.trim() : next;
899
+ }
900
+ if (Array.isArray(value)) return value.map((item) => substituteValue(item, values));
901
+ if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, substituteValue(item, values, key)]));
902
+ return value;
903
+ }
904
+ function substituteString(value, values) {
905
+ if (!value.includes("${{")) return value;
906
+ const expression = parseCompositeRunExpression(value);
907
+ if (!expression) return value;
908
+ try {
909
+ const expr = parseExpression(expression);
910
+ const replacement = directReplacement(expr, values);
911
+ if (replacement !== void 0) return replacement;
912
+ const simplified = printExpression(expr, values);
913
+ const staticValue = staticExpression(parseExpression(simplified), values);
914
+ return staticValue ? staticValue.value : `\${{ ${simplified} }}`;
915
+ } catch {
916
+ return value;
917
+ }
918
+ }
919
+ function normalizeInputValue(value) {
920
+ if (value === void 0 || value === null) return "";
921
+ return value;
922
+ }
923
+ function parseCompositeRunExpression(value) {
924
+ const expression = expressionBody(value.trim());
925
+ if (expression !== void 0) return expression;
926
+ const result = parseActionTemplate(`runs:
927
+ using: composite
928
+ steps:
929
+ - run: ${JSON.stringify(value)}
930
+ `, "actionspack-substitute/action.yml");
931
+ if (!result.value) return;
932
+ const token = findToken(result.value, [
933
+ "runs",
934
+ "steps",
935
+ "0",
936
+ "run"
937
+ ]);
938
+ return token && isBasicExpression(token) ? token.expression : void 0;
939
+ }
940
+ function findToken(token, path) {
941
+ if (path.length === 0) return token;
942
+ const [head, ...rest] = path;
943
+ if (isMapping(token)) {
944
+ const next = token.find(head);
945
+ return next ? findToken(next, rest) : void 0;
946
+ }
947
+ if (isSequence(token)) {
948
+ const index = Number(head);
949
+ return Number.isInteger(index) ? findToken(token.get(index), rest) : void 0;
950
+ }
951
+ }
952
+ function directReplacement(expr, values) {
953
+ return valueForIndexAccess(expr, values);
954
+ }
955
+ //#endregion
956
+ //#region src/pack.ts
957
+ async function pack(options = {}) {
958
+ if ((await discoverConfig(resolveCwd(options.cwd), options)).entries.length === 0) throw new Error("No workflow source files found in .github/workflows/src");
959
+ return packScanned(await scan(options), options);
960
+ }
961
+ async function packScanned(scanResult, options = {}) {
962
+ const cwd = resolveCwd(options.cwd);
963
+ const github = options.github ?? new HttpGitHubClient();
964
+ for (const entry of scanResult.entries) {
965
+ const packed = await packWorkflow(await readWorkflowEntry(cwd, entry), scanResult.lockfile, github);
966
+ assertNoRemoteUses(packed, entry.output);
967
+ await writeWorkflow(cwd, entry.output, packed);
968
+ }
969
+ options.stdout?.write(`Packed ${scanResult.entries.length} workflow${scanResult.entries.length === 1 ? "" : "s"}\n`);
970
+ return scanResult;
971
+ }
972
+ async function verify(options = {}) {
973
+ const cwd = resolveCwd(options.cwd);
974
+ const config = await discoverConfig(cwd, options);
975
+ const lockfile = await readLockfile(cwd);
976
+ const github = options.github ?? new HttpGitHubClient();
977
+ for (const entry of config.entries) {
978
+ const expected = await packWorkflow(await readWorkflowEntry(cwd, entry), lockfile, github);
979
+ assertNoRemoteUses(expected, entry.output);
980
+ if (await readFile(path.join(cwd, entry.output), "utf8").catch(() => {
981
+ throw new Error(`Missing generated workflow: ${entry.output}`);
982
+ }) !== stringifyWorkflowYaml(expected)) throw new Error(`Generated workflow is stale: ${entry.output}`);
983
+ }
984
+ return {
985
+ lockfile,
986
+ entries: config.entries
987
+ };
988
+ }
989
+ async function packWorkflow(workflow, lockfile, github) {
990
+ const packed = substituteValue(structuredClone(workflow), {});
991
+ const jobs = asRecord(packed.jobs);
992
+ if (!jobs) return packed;
993
+ const nextJobs = {};
994
+ const needsReplacements = {};
995
+ for (const [jobId, jobValue] of Object.entries(jobs)) {
996
+ const job = asRecord(jobValue);
997
+ if (!job) {
998
+ nextJobs[jobId] = jobValue;
999
+ continue;
1000
+ }
1001
+ if (typeof job.uses === "string") {
1002
+ if (!isRemoteUses(job.uses)) throw new Error(`Local reusable workflows are not transformed yet: jobs.${jobId}.uses`);
1003
+ const remote = parseRemoteUses(job.uses, "reusable-workflow");
1004
+ const item = requirePackage(lockfile, remote);
1005
+ if (item.external) {
1006
+ nextJobs[jobId] = {
1007
+ ...job,
1008
+ uses: pinnedUses(remote, item.resolved)
1009
+ };
1010
+ continue;
1011
+ }
1012
+ const inlined = await inlineReusableWorkflow(jobId, job, lockfile, github);
1013
+ Object.assign(nextJobs, inlined.jobs);
1014
+ needsReplacements[jobId] = inlined.jobIds;
1015
+ continue;
1016
+ }
1017
+ nextJobs[jobId] = await packJob(job, lockfile, github);
1018
+ }
1019
+ packed.jobs = optimizeJobs(rewritePackedNeeds(nextJobs, needsReplacements));
1020
+ return substituteValue(packed, {});
1021
+ }
1022
+ async function packJob(job, lockfile, github) {
1023
+ const next = { ...job };
1024
+ next.steps = await packSteps(asArray(job.steps), lockfile, github);
1025
+ return optimizeJob(next);
1026
+ }
1027
+ async function packSteps(steps, lockfile, github) {
1028
+ const packed = [];
1029
+ for (const stepValue of steps) {
1030
+ const step = asRecord(stepValue);
1031
+ if (!step || typeof step.uses !== "string") {
1032
+ const next = optimizeStep(substituteValue(stepValue, {}));
1033
+ if (next !== void 0) packed.push(next);
1034
+ continue;
1035
+ }
1036
+ if (!isRemoteUses(step.uses)) {
1037
+ const next = optimizeStep(stepValue);
1038
+ if (next !== void 0) packed.push(next);
1039
+ continue;
1040
+ }
1041
+ packed.push(...await inlineCompositeStep(step, lockfile, github));
1042
+ }
1043
+ return packed;
1044
+ }
1045
+ async function inlineCompositeStep(callerStep, lockfile, github) {
1046
+ const remote = parseRemoteUses(String(callerStep.uses), "action");
1047
+ const item = requirePackage(lockfile, remote);
1048
+ if (item.type !== "composite") {
1049
+ if (item.external) return [{
1050
+ ...callerStep,
1051
+ uses: pinnedUses(remote, item.resolved)
1052
+ }];
1053
+ throw new Error(`Expected composite action for ${callerStep.uses}`);
1054
+ }
1055
+ const metadata = await readActionMetadata(github, item, item.resolved);
1056
+ const action = parseActionMap(metadata.content, metadata.path);
1057
+ if (asRecord(action.outputs) && typeof callerStep.id === "string") throw new Error(`Composite action outputs are not supported for step id ${callerStep.id}`);
1058
+ const values = collectActionInputValues(action, callerStep, String(callerStep.uses));
1059
+ return (await packSteps(substituteValue(asArray(asRecord(action.runs).steps), values), lockfile, github)).map((stepValue, index) => optimizeStep(applyCallerStepFields(stepValue, callerStep, index))).filter((stepValue) => stepValue !== void 0);
1060
+ }
1061
+ async function inlineReusableWorkflow(callerJobId, callerJob, lockfile, github) {
1062
+ const item = requirePackage(lockfile, parseRemoteUses(String(callerJob.uses), "reusable-workflow"));
1063
+ if (item.type !== "reusable-workflow") throw new Error(`Expected reusable workflow for ${callerJob.uses}`);
1064
+ if (callerJob.secrets === "inherit") throw new Error(`secrets: inherit is not supported for ${callerJobId}`);
1065
+ rejectUnsupportedReusableCallerKeys(callerJob, callerJobId);
1066
+ const content = await github.readFile(item.owner, item.repo, item.resolved, item.path);
1067
+ if (!content) throw new Error(`Missing reusable workflow ${item.owner}/${item.repo}/${item.path}@${item.resolved}`);
1068
+ const workflow = parseWorkflowMap(content, item.path);
1069
+ const call = workflowCallConfig(workflow);
1070
+ if (!call) throw new Error(`Reusable workflow ${String(callerJob.uses)} must use workflow_call`);
1071
+ if (asRecord(call.outputs)) throw new Error(`Reusable workflow outputs are not supported for ${callerJobId}`);
1072
+ const values = collectReusableValues(call, callerJob, callerJobId);
1073
+ const remoteJobs = asRecord(workflow.jobs);
1074
+ if (!remoteJobs) throw new Error(`Reusable workflow has no jobs: ${String(callerJob.uses)}`);
1075
+ const copied = {};
1076
+ const copiedJobIds = Object.keys(remoteJobs).map((remoteJobId) => `${callerJobId}-${remoteJobId}`);
1077
+ for (const [remoteJobId, remoteJobValue] of Object.entries(remoteJobs)) {
1078
+ const remoteJob = asRecord(remoteJobValue);
1079
+ if (!remoteJob) throw new Error(`Reusable workflow job must be a mapping: ${remoteJobId}`);
1080
+ if (remoteJob.uses) throw new Error(`Nested reusable workflows are not supported: ${callerJobId}-${remoteJobId}`);
1081
+ const transformed = await packJob(substituteValue(remoteJob, values), lockfile, github);
1082
+ transformed.needs = rewriteReusableNeeds(remoteJob.needs, callerJob.needs, callerJobId);
1083
+ copied[`${callerJobId}-${remoteJobId}`] = optimizeJob(transformed);
1084
+ }
1085
+ return {
1086
+ jobIds: copiedJobIds,
1087
+ jobs: copied
1088
+ };
1089
+ }
1090
+ function collectActionInputValues(action, callerStep, uses) {
1091
+ const values = {};
1092
+ const supplied = asRecord(callerStep.with) ?? {};
1093
+ const inputs = asRecord(action.inputs) ?? {};
1094
+ for (const [name, inputValue] of Object.entries(inputs)) {
1095
+ const input = asRecord(inputValue) ?? {};
1096
+ const suppliedValue = supplied[name];
1097
+ const defaultValue = input.default;
1098
+ const required = input.required === true || input.required === "true";
1099
+ if (suppliedValue === void 0 && defaultValue === void 0 && required) throw new Error(`Missing required input ${name} for ${uses}`);
1100
+ values[`inputs.${name}`] = normalizeInputValue(suppliedValue ?? defaultValue);
1101
+ }
1102
+ for (const [name, value] of Object.entries(supplied)) values[`inputs.${name}`] = normalizeInputValue(value);
1103
+ return values;
1104
+ }
1105
+ function collectReusableValues(call, callerJob, jobId) {
1106
+ const values = {};
1107
+ const supplied = asRecord(callerJob.with) ?? {};
1108
+ const inputs = asRecord(call.inputs) ?? {};
1109
+ for (const [name, inputValue] of Object.entries(inputs)) {
1110
+ const input = asRecord(inputValue) ?? {};
1111
+ const suppliedValue = supplied[name];
1112
+ const defaultValue = input.default;
1113
+ const required = input.required === true || input.required === "true";
1114
+ if (suppliedValue === void 0 && defaultValue === void 0 && required) throw new Error(`Missing required workflow input ${name} for ${jobId}`);
1115
+ values[`inputs.${name}`] = normalizeInputValue(suppliedValue ?? defaultValue);
1116
+ }
1117
+ for (const [name, value] of Object.entries(supplied)) values[`inputs.${name}`] = normalizeInputValue(value);
1118
+ const secrets = asRecord(callerJob.secrets) ?? {};
1119
+ const declaredSecrets = asRecord(call.secrets) ?? {};
1120
+ for (const name of Object.keys(declaredSecrets)) {
1121
+ if (secrets[name] === void 0) {
1122
+ const declaration = asRecord(declaredSecrets[name]);
1123
+ if (declaration?.required === true || declaration?.required === "true") throw new Error(`Missing required workflow secret ${name} for ${jobId}`);
1124
+ continue;
1125
+ }
1126
+ values[`secrets.${name}`] = normalizeInputValue(secrets[name]);
1127
+ }
1128
+ return values;
1129
+ }
1130
+ function applyCallerStepFields(stepValue, callerStep, index) {
1131
+ const step = asRecord(stepValue);
1132
+ if (!step) return stepValue;
1133
+ const next = { ...step };
1134
+ if (index === 0 && typeof callerStep.name === "string" && typeof next.name !== "string") next.name = callerStep.name;
1135
+ if (callerStep.if !== void 0) next.if = next.if === void 0 ? callerStep.if : `(${String(callerStep.if)}) && (${String(next.if)})`;
1136
+ if (asRecord(callerStep.env)) next.env = {
1137
+ ...asRecord(callerStep.env),
1138
+ ...asRecord(next.env)
1139
+ };
1140
+ return next;
1141
+ }
1142
+ function rewriteReusableNeeds(remoteNeeds, callerNeeds, callerJobId) {
1143
+ const rewrite = (value) => `${callerJobId}-${value}`;
1144
+ const rewrittenRemoteNeeds = typeof remoteNeeds === "string" ? rewrite(remoteNeeds) : Array.isArray(remoteNeeds) ? remoteNeeds.map((item) => rewrite(String(item))) : [];
1145
+ const needs = [...typeof callerNeeds === "string" ? [callerNeeds] : Array.isArray(callerNeeds) ? callerNeeds : [], ...rewrittenRemoteNeeds];
1146
+ return needs.length > 0 ? needs : void 0;
1147
+ }
1148
+ function rewritePackedNeeds(jobs, replacements) {
1149
+ if (Object.keys(replacements).length === 0) return jobs;
1150
+ return Object.fromEntries(Object.entries(jobs).map(([jobId, jobValue]) => {
1151
+ const job = asRecord(jobValue);
1152
+ if (!job) return [jobId, jobValue];
1153
+ return [jobId, optimizeJob({
1154
+ ...job,
1155
+ needs: rewriteNeedsValue(job.needs, replacements)
1156
+ })];
1157
+ }));
1158
+ }
1159
+ function rewriteNeedsValue(value, replacements) {
1160
+ if (value === void 0) return;
1161
+ const originalIsString = typeof value === "string";
1162
+ const rewritten = (originalIsString ? [value] : Array.isArray(value) ? value.map(String) : []).flatMap((need) => replacements[need] ?? [need]);
1163
+ const unique = [...new Set(rewritten)];
1164
+ if (unique.length === 0) return;
1165
+ return originalIsString && unique.length === 1 ? unique[0] : unique;
1166
+ }
1167
+ function workflowCallConfig(workflow) {
1168
+ const on = Reflect.get(workflow, "on");
1169
+ if (on === "workflow_call") return {};
1170
+ if (Array.isArray(on) && on.includes("workflow_call")) return {};
1171
+ const onMap = asRecord(on);
1172
+ if (!onMap) return;
1173
+ const call = onMap.workflow_call;
1174
+ if (call === null) return {};
1175
+ return asRecord(call) ?? {};
1176
+ }
1177
+ function rejectUnsupportedReusableCallerKeys(callerJob, jobId) {
1178
+ const allowed = new Set([
1179
+ "uses",
1180
+ "with",
1181
+ "secrets",
1182
+ "needs",
1183
+ "if",
1184
+ "permissions",
1185
+ "name"
1186
+ ]);
1187
+ for (const key of Object.keys(callerJob)) if (!allowed.has(key)) throw new Error(`Unsupported reusable workflow caller key jobs.${jobId}.${key}`);
1188
+ }
1189
+ function requirePackage(lockfile, remote) {
1190
+ const item = lockfile.packages[remote.package];
1191
+ if (!item) throw new Error(`Missing lockfile package for ${remote.package}`);
1192
+ return item;
1193
+ }
1194
+ function assertNoRemoteUses(value, file = "workflow") {
1195
+ const visit = (item, trail) => {
1196
+ if (Array.isArray(item)) {
1197
+ item.forEach((child, index) => visit(child, `${trail}[${index}]`));
1198
+ return;
1199
+ }
1200
+ const record = asRecord(item);
1201
+ if (!record) return;
1202
+ if (isRemoteUses(record.uses) && !isPinnedRemoteUses(record.uses)) throw new Error(`Packed workflow contains remote action reference at ${file}${trail}.uses: ${record.uses}`);
1203
+ for (const [key, child] of Object.entries(record)) visit(child, `${trail}.${key}`);
1204
+ };
1205
+ visit(value, "");
1206
+ }
1207
+ //#endregion
1208
+ //#region src/commands.ts
1209
+ const execFileAsync = promisify(execFile);
1210
+ async function update(options = {}) {
1211
+ const cwd = resolveCwd(options.cwd);
1212
+ const refreshPackages = selectRefreshPackages(await readLockfile(cwd), options.packageName);
1213
+ const scanResult = await scan({
1214
+ ...options,
1215
+ cwd,
1216
+ refreshPackages
1217
+ });
1218
+ if (!options.lockfileOnly) await packScanned(scanResult, options);
1219
+ }
1220
+ async function tree(options = {}) {
1221
+ const lockfile = await readLockfile(options.cwd);
1222
+ const lines = [];
1223
+ for (const [source, entry] of Object.entries(lockfile.entries)) {
1224
+ lines.push(source);
1225
+ appendDependencyTree(lines, lockfile, entry.dependencies, "");
1226
+ }
1227
+ const output = `${lines.join("\n")}\n`;
1228
+ options.stdout?.write(output);
1229
+ return output;
1230
+ }
1231
+ async function why(packageName, options = {}) {
1232
+ const lockfile = await readLockfile(options.cwd);
1233
+ const matches = new Set(Object.keys(lockfile.packages).filter((key) => matchesPackageSelector(key, packageName)));
1234
+ if (matches.size === 0) throw new Error(`Package is not in the lockfile: ${packageName}`);
1235
+ const paths = [];
1236
+ for (const [source, entry] of Object.entries(lockfile.entries)) for (const dependency of entry.dependencies) collectWhyPaths(lockfile, dependency, matches, [source], paths);
1237
+ const output = paths.length === 0 ? `${packageName} is not reachable\n` : `${packageName} is used by:\n\n${paths.map(formatPath).join("\n\n")}\n`;
1238
+ options.stdout?.write(output);
1239
+ return output;
1240
+ }
1241
+ async function diff(options = {}) {
1242
+ const cwd = resolveCwd(options.cwd);
1243
+ const current = await readLockfile(cwd);
1244
+ const result = diffLockfiles(await readHeadLockfile(cwd), current);
1245
+ if (options.json) {
1246
+ options.stdout?.write(`${JSON.stringify(result, null, 2)}\n`);
1247
+ return result;
1248
+ }
1249
+ const output = formatDiff(result);
1250
+ options.stdout?.write(output);
1251
+ return output;
1252
+ }
1253
+ function selectRefreshPackages(lockfile, selector) {
1254
+ const keys = Object.keys(lockfile.packages);
1255
+ if (!selector) return new Set(keys);
1256
+ const selected = keys.filter((key) => matchesPackageSelector(key, selector));
1257
+ if (selected.length === 0) throw new Error(`Package is not in the lockfile: ${selector}`);
1258
+ return new Set(selected);
1259
+ }
1260
+ function appendDependencyTree(lines, lockfile, dependencies, prefix) {
1261
+ dependencies.forEach((dependency, index) => {
1262
+ const last = index === dependencies.length - 1;
1263
+ const item = lockfile.packages[dependency.package];
1264
+ const label = item ? `${item.owner}/${item.repo}${item.path === "." ? "" : `/${item.path}`}@${item.requested} (${item.resolved})` : `${dependency.package}@${dependency.requested}`;
1265
+ lines.push(`${prefix}${last ? "└─" : "├─"} ${label}`);
1266
+ if (item) appendDependencyTree(lines, lockfile, item.dependencies, `${prefix}${last ? " " : "│ "}`);
1267
+ });
1268
+ }
1269
+ function collectWhyPaths(lockfile, dependency, matches, current, paths) {
1270
+ const item = lockfile.packages[dependency.package];
1271
+ const label = item ? `${item.owner}/${item.repo}${item.path === "." ? "" : `/${item.path}`}` : dependency.package;
1272
+ const next = [...current, label];
1273
+ if (matches.has(dependency.package)) paths.push(next);
1274
+ item?.dependencies.forEach((child) => collectWhyPaths(lockfile, child, matches, next, paths));
1275
+ }
1276
+ function formatPath(path) {
1277
+ return path.map((item, index) => `${" ".repeat(index)}${index === 0 ? item : `└─ ${item}`}`).join("\n");
1278
+ }
1279
+ async function readHeadLockfile(cwd) {
1280
+ try {
1281
+ const { stdout } = await execFileAsync("git", ["show", `HEAD:${LOCKFILE_PATH}`], { cwd });
1282
+ const { parse } = await import("yaml");
1283
+ return parse(stdout) ?? {
1284
+ lockfileVersion: 1,
1285
+ entries: {},
1286
+ packages: {}
1287
+ };
1288
+ } catch {
1289
+ return {
1290
+ lockfileVersion: 1,
1291
+ entries: {},
1292
+ packages: {}
1293
+ };
1294
+ }
1295
+ }
1296
+ function diffLockfiles(previous, current) {
1297
+ const previousKeys = new Set(Object.keys(previous.packages));
1298
+ const currentKeys = new Set(Object.keys(current.packages));
1299
+ return {
1300
+ added: [...currentKeys].filter((key) => !previousKeys.has(key)).toSorted(),
1301
+ removed: [...previousKeys].filter((key) => !currentKeys.has(key)).toSorted(),
1302
+ changed: [...currentKeys].filter((key) => previousKeys.has(key)).flatMap((key) => {
1303
+ const oldPackage = previous.packages[key];
1304
+ const newPackage = current.packages[key];
1305
+ const dependencyChanged = stringifyYaml(oldPackage.dependencies) !== stringifyYaml(newPackage.dependencies);
1306
+ if (oldPackage.resolved === newPackage.resolved && !dependencyChanged) return [];
1307
+ return [{
1308
+ package: key,
1309
+ oldResolved: oldPackage.resolved,
1310
+ newResolved: newPackage.resolved,
1311
+ dependencyChanged
1312
+ }];
1313
+ }).toSorted((a, b) => a.package.localeCompare(b.package))
1314
+ };
1315
+ }
1316
+ function formatDiff(result) {
1317
+ const lines = [];
1318
+ for (const item of result.changed) {
1319
+ lines.push(item.package, `old: ${item.oldResolved}`, `new: ${item.newResolved}`);
1320
+ if (item.dependencyChanged) lines.push("transitive changes: changed");
1321
+ lines.push("");
1322
+ }
1323
+ if (result.added.length > 0) lines.push("added:", ...result.added.map((item) => `- ${item}`), "");
1324
+ if (result.removed.length > 0) lines.push("removed:", ...result.removed.map((item) => `- ${item}`), "");
1325
+ if (lines.length === 0) return "No lockfile changes\n";
1326
+ return `${lines.join("\n").trimEnd()}\n`;
1327
+ }
1328
+ //#endregion
1329
+ export { why as a, packWorkflow as c, collectWorkflowDependencies as d, scan as f, update as i, verify as l, diffLockfiles as n, assertNoRemoteUses as o, tree as r, pack as s, diff as t, collectStepDependencies as u };