actions-up 0.0.1 → 1.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.
Files changed (77) hide show
  1. package/bin/actions-up.js +5 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +67 -0
  4. package/dist/core/api/check-updates.d.ts +10 -0
  5. package/dist/core/api/check-updates.js +139 -0
  6. package/dist/core/api/client.d.ts +79 -0
  7. package/dist/core/api/client.js +187 -0
  8. package/dist/core/ast/guards/has-range.d.ts +10 -0
  9. package/dist/core/ast/guards/has-range.js +4 -0
  10. package/dist/core/ast/guards/is-node.d.ts +8 -0
  11. package/dist/core/ast/guards/is-node.js +4 -0
  12. package/dist/core/ast/guards/is-pair.d.ts +8 -0
  13. package/dist/core/ast/guards/is-pair.js +4 -0
  14. package/dist/core/ast/guards/is-scalar.d.ts +8 -0
  15. package/dist/core/ast/guards/is-scalar.js +4 -0
  16. package/dist/core/ast/guards/is-yaml-map.d.ts +8 -0
  17. package/dist/core/ast/guards/is-yaml-map.js +4 -0
  18. package/dist/core/ast/guards/is-yaml-sequence.d.ts +8 -0
  19. package/dist/core/ast/guards/is-yaml-sequence.js +4 -0
  20. package/dist/core/ast/scanners/scan-composite-action-ast.d.ts +14 -0
  21. package/dist/core/ast/scanners/scan-composite-action-ast.js +18 -0
  22. package/dist/core/ast/scanners/scan-workflow-ast.d.ts +14 -0
  23. package/dist/core/ast/scanners/scan-workflow-ast.js +23 -0
  24. package/dist/core/ast/update/apply-updates.d.ts +7 -0
  25. package/dist/core/ast/update/apply-updates.js +40 -0
  26. package/dist/core/ast/utils/extract-uses-from-steps.d.ts +13 -0
  27. package/dist/core/ast/utils/extract-uses-from-steps.js +24 -0
  28. package/dist/core/ast/utils/find-map-pair.d.ts +12 -0
  29. package/dist/core/ast/utils/find-map-pair.js +10 -0
  30. package/dist/core/ast/utils/get-line-number.d.ts +10 -0
  31. package/dist/core/ast/utils/get-line-number.js +9 -0
  32. package/dist/core/constants.d.ts +4 -0
  33. package/dist/core/constants.js +4 -0
  34. package/dist/core/fs/is-yaml-file.d.ts +7 -0
  35. package/dist/core/fs/is-yaml-file.js +4 -0
  36. package/dist/core/fs/read-yaml-document.d.ts +11 -0
  37. package/dist/core/fs/read-yaml-document.js +11 -0
  38. package/dist/core/index.d.ts +3 -0
  39. package/dist/core/index.js +4 -0
  40. package/dist/core/interactive/format-version.d.ts +7 -0
  41. package/dist/core/interactive/format-version.js +5 -0
  42. package/dist/core/interactive/pad-string.d.ts +8 -0
  43. package/dist/core/interactive/pad-string.js +9 -0
  44. package/dist/core/interactive/prompt-update-selection.d.ts +2 -0
  45. package/dist/core/interactive/prompt-update-selection.js +203 -0
  46. package/dist/core/interactive/strip-ansi.d.ts +7 -0
  47. package/dist/core/interactive/strip-ansi.js +21 -0
  48. package/dist/core/parsing/parse-action-reference.d.ts +30 -0
  49. package/dist/core/parsing/parse-action-reference.js +34 -0
  50. package/dist/core/scan-action-file.d.ts +10 -0
  51. package/dist/core/scan-action-file.js +7 -0
  52. package/dist/core/scan-github-actions.d.ts +17 -0
  53. package/dist/core/scan-github-actions.js +116 -0
  54. package/dist/core/scan-workflow-file.d.ts +9 -0
  55. package/dist/core/scan-workflow-file.js +7 -0
  56. package/dist/core/schema/composite/is-composite-action-runs.d.ts +8 -0
  57. package/dist/core/schema/composite/is-composite-action-runs.js +6 -0
  58. package/dist/core/schema/composite/is-composite-action-step.d.ts +8 -0
  59. package/dist/core/schema/composite/is-composite-action-structure.d.ts +9 -0
  60. package/dist/core/schema/composite/is-composite-action-structure.js +6 -0
  61. package/dist/core/schema/workflow/is-workflow-job.d.ts +8 -0
  62. package/dist/core/schema/workflow/is-workflow-step.d.ts +8 -0
  63. package/dist/core/schema/workflow/is-workflow-structure.d.ts +8 -0
  64. package/dist/core/schema/workflow/is-workflow-structure.js +6 -0
  65. package/dist/package.js +2 -0
  66. package/dist/types/action-update.d.ts +21 -0
  67. package/dist/types/composite-action-runs.d.ts +12 -0
  68. package/dist/types/composite-action-step.d.ts +23 -0
  69. package/dist/types/composite-action-structure.d.ts +21 -0
  70. package/dist/types/github-action.d.ts +23 -0
  71. package/dist/types/scan-result.d.ts +12 -0
  72. package/dist/types/workflow-job.d.ts +18 -0
  73. package/dist/types/workflow-step.d.ts +20 -0
  74. package/dist/types/workflow-structure.d.ts +15 -0
  75. package/license.md +20 -0
  76. package/package.json +52 -1
  77. package/readme.md +175 -0
@@ -0,0 +1,40 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ async function applyUpdates(updates) {
3
+ let updatesByFile = /* @__PURE__ */ new Map();
4
+ for (let update of updates) {
5
+ let { file } = update.action;
6
+ if (!file) continue;
7
+ let fileUpdates = updatesByFile.get(file) ?? [];
8
+ fileUpdates.push(update);
9
+ updatesByFile.set(file, fileUpdates);
10
+ }
11
+ let filePromises = [...updatesByFile.entries()].map(async ([filePath, fileUpdates]) => {
12
+ let content = await readFile(filePath, "utf8");
13
+ for (let update of fileUpdates) {
14
+ if (!update.latestSha) continue;
15
+ function escapeRegExp(string_) {
16
+ return string_.replaceAll(/[$()*+\-./?[\\\]^{|}]/gu, String.raw`\$&`);
17
+ }
18
+ let escapedName = escapeRegExp(update.action.name);
19
+ let escapedVersion = update.currentVersion ? escapeRegExp(update.currentVersion) : "";
20
+ if (escapedName.includes("\n") || escapedName.includes("\r")) {
21
+ console.error(`Invalid action name: ${update.action.name}`);
22
+ continue;
23
+ }
24
+ if (escapedVersion && (escapedVersion.includes("\n") || escapedVersion.includes("\r"))) {
25
+ console.error(`Invalid version: ${update.currentVersion}`);
26
+ continue;
27
+ }
28
+ if (!/^[\da-f]{40}$/iu.test(update.latestSha)) {
29
+ console.error(`Invalid SHA format: ${update.latestSha}`);
30
+ continue;
31
+ }
32
+ let pattern = new RegExp(`(^\\s*-?\\s*uses:\\s*)(['"]?)(${escapedName})@${escapedVersion}\\2(\\s*#[^\\n]*)?`, "gm");
33
+ let replacement = `$1$2$3@${update.latestSha}$2 # ${update.latestVersion}`;
34
+ content = content.replace(pattern, replacement);
35
+ }
36
+ await writeFile(filePath, content, "utf8");
37
+ });
38
+ await Promise.all(filePromises);
39
+ }
40
+ export { applyUpdates };
@@ -0,0 +1,13 @@
1
+ import { GitHubAction } from '../../../types/github-action';
2
+ /**
3
+ * Extracts GitHub Action references from a steps YAML sequence.
4
+ *
5
+ * Uses the AST to locate the 'uses' key for precise line numbers and the JSON
6
+ * representation to validate the presence and type of the 'uses' field.
7
+ *
8
+ * @param stepsNode - YAML sequence node containing workflow/action steps.
9
+ * @param filePath - Path of the file being scanned (for metadata).
10
+ * @param content - Original YAML file content (for line number calculation).
11
+ * @returns List of discovered GitHub actions.
12
+ */
13
+ export declare function extractUsesFromSteps(stepsNode: unknown, filePath: string, content: string): GitHubAction[];
@@ -0,0 +1,24 @@
1
+ import { parseActionReference } from "../../parsing/parse-action-reference.js";
2
+ import { isYAMLSequence } from "../guards/is-yaml-sequence.js";
3
+ import { getLineNumberForKey } from "./get-line-number.js";
4
+ import { isYAMLMap } from "../guards/is-yaml-map.js";
5
+ import { isScalar } from "../guards/is-scalar.js";
6
+ import { isNode } from "../guards/is-node.js";
7
+ import { isPair } from "../guards/is-pair.js";
8
+ function extractUsesFromSteps(stepsNode, filePath, content) {
9
+ if (!isYAMLSequence(stepsNode)) return [];
10
+ let actions = [];
11
+ for (let stepNode of stepsNode.items) {
12
+ if (!isYAMLMap(stepNode) || !isNode(stepNode)) continue;
13
+ let step = stepNode.toJSON();
14
+ if (step === null || typeof step !== "object" || Array.isArray(step)) continue;
15
+ let stepObject = step;
16
+ if (typeof stepObject["uses"] !== "string") continue;
17
+ let usesPair = stepNode.items.find((item) => isPair(item) && isScalar(item.key) && item.key.value === "uses");
18
+ let lineNumber = usesPair?.key ? getLineNumberForKey(content, usesPair.key) : 0;
19
+ let action = parseActionReference(stepObject["uses"], filePath, lineNumber);
20
+ if (action) actions.push(action);
21
+ }
22
+ return actions;
23
+ }
24
+ export { extractUsesFromSteps };
@@ -0,0 +1,12 @@
1
+ import { Pair } from 'yaml';
2
+ /**
3
+ * Finds a key-value Pair by its key within a YAML map node.
4
+ *
5
+ * Returns null when the provided node is not a YAML map or when no entry with
6
+ * the given key exists.
7
+ *
8
+ * @param map - Candidate YAML node expected to be a map.
9
+ * @param key - Key name to locate.
10
+ * @returns Matching Pair when found, otherwise null.
11
+ */
12
+ export declare function findMapPair(map: unknown, key: string): Pair | null;
@@ -0,0 +1,10 @@
1
+ import { isYAMLMap } from "../guards/is-yaml-map.js";
2
+ import { isScalar } from "../guards/is-scalar.js";
3
+ import { isPair } from "../guards/is-pair.js";
4
+ function findMapPair(map, key) {
5
+ if (!isYAMLMap(map) || !Array.isArray(map.items)) return null;
6
+ let yamlMap = map;
7
+ let pair = yamlMap.items.find((item) => isPair(item) && isScalar(item.key) && item.key.value === key);
8
+ return pair ?? null;
9
+ }
10
+ export { findMapPair };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Calculates a 1-based line number for a YAML key node using its range.
3
+ *
4
+ * Returns 0 when the range is missing or malformed.
5
+ *
6
+ * @param content - Original file content used to compute line breaks.
7
+ * @param keyNode - YAML key node that may include a `range` tuple.
8
+ * @returns 1-based line number, or 0 when unknown.
9
+ */
10
+ export declare function getLineNumberForKey(content: string, keyNode: unknown): number;
@@ -0,0 +1,9 @@
1
+ import { hasRange } from "../guards/has-range.js";
2
+ function getLineNumberForKey(content, keyNode) {
3
+ if (hasRange(keyNode) && keyNode.range) {
4
+ let [offset] = keyNode.range;
5
+ if (typeof offset === "number" && Number.isFinite(offset)) return content.slice(0, Math.max(0, offset)).split("\n").length;
6
+ }
7
+ return 0;
8
+ }
9
+ export { getLineNumberForKey };
@@ -0,0 +1,4 @@
1
+ /** Constants for directory names used in the project. */
2
+ export declare const GITHUB_DIRECTORY: ".github";
3
+ export declare const WORKFLOWS_DIRECTORY: "workflows";
4
+ export declare const ACTIONS_DIRECTORY: "actions";
@@ -0,0 +1,4 @@
1
+ const GITHUB_DIRECTORY = ".github";
2
+ const WORKFLOWS_DIRECTORY = "workflows";
3
+ const ACTIONS_DIRECTORY = "actions";
4
+ export { ACTIONS_DIRECTORY, GITHUB_DIRECTORY, WORKFLOWS_DIRECTORY };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Checks if a file is a YAML file.
3
+ *
4
+ * @param filePath - The path to the file.
5
+ * @returns True if the file is a YAML file, false otherwise.
6
+ */
7
+ export declare function isYamlFile(filePath: string): boolean;
@@ -0,0 +1,4 @@
1
+ function isYamlFile(filePath) {
2
+ return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
3
+ }
4
+ export { isYamlFile };
@@ -0,0 +1,11 @@
1
+ import { Document } from 'yaml';
2
+ /**
3
+ * Reads a YAML file and returns both its raw content and parsed Document.
4
+ *
5
+ * @param filePath - Path to the YAML file.
6
+ * @returns Parsed YAML document along with original content.
7
+ */
8
+ export declare function readYamlDocument(filePath: string): Promise<{
9
+ document: Document;
10
+ content: string;
11
+ }>;
@@ -0,0 +1,11 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseDocument } from "yaml";
3
+ async function readYamlDocument(filePath) {
4
+ let content = await readFile(filePath, "utf8");
5
+ let document = parseDocument(content);
6
+ return {
7
+ document,
8
+ content
9
+ };
10
+ }
11
+ export { readYamlDocument };
@@ -0,0 +1,3 @@
1
+ export { scanGitHubActions } from './scan-github-actions';
2
+ export { applyUpdates } from './ast/update/apply-updates';
3
+ export { checkUpdates } from './api/check-updates';
@@ -0,0 +1,4 @@
1
+ import { applyUpdates } from "./ast/update/apply-updates.js";
2
+ import { checkUpdates } from "./api/check-updates.js";
3
+ import { scanGitHubActions } from "./scan-github-actions.js";
4
+ export { applyUpdates, checkUpdates, scanGitHubActions };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Formats a version string for display, handling null/undefined values.
3
+ *
4
+ * @param version - Version string or null/undefined.
5
+ * @returns Formatted version string or 'unknown' placeholder.
6
+ */
7
+ export declare function formatVersion(version: undefined | string | null): string;
@@ -0,0 +1,5 @@
1
+ import pc from "picocolors";
2
+ function formatVersion(version) {
3
+ return version ?? pc.gray("unknown");
4
+ }
5
+ export { formatVersion };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Pad a string to a specific length.
3
+ *
4
+ * @param string - String to pad.
5
+ * @param length - Target length.
6
+ * @returns Padded string.
7
+ */
8
+ export declare function padString(string: string, length: number): string;
@@ -0,0 +1,9 @@
1
+ import { stripAnsi } from "./strip-ansi.js";
2
+ function padString(string, length) {
3
+ let stripped = stripAnsi(string);
4
+ let diff = length - stripped.length;
5
+ if (diff <= 0) return string;
6
+ let padding = " ".repeat(diff);
7
+ return string + padding;
8
+ }
9
+ export { padString };
@@ -0,0 +1,2 @@
1
+ import { ActionUpdate } from '../../types/action-update';
2
+ export declare function promptUpdateSelection(updates: ActionUpdate[]): Promise<ActionUpdate[] | null>;
@@ -0,0 +1,203 @@
1
+ import { formatVersion } from "./format-version.js";
2
+ import { GITHUB_DIRECTORY } from "../constants.js";
3
+ import { stripAnsi } from "./strip-ansi.js";
4
+ import { padString } from "./pad-string.js";
5
+ import "node:worker_threads";
6
+ import pc from "picocolors";
7
+ import enquirer from "enquirer";
8
+ import path from "node:path";
9
+ const MIN_ACTION_WIDTH = 56;
10
+ const MIN_CURRENT_WIDTH = 10;
11
+ async function promptUpdateSelection(updates) {
12
+ if (updates.length === 0) return null;
13
+ let outdated = updates.filter((update) => update.hasUpdate);
14
+ if (outdated.length === 0) {
15
+ console.info(pc.green("✓ All actions are up to date!"));
16
+ return null;
17
+ }
18
+ let groups = /* @__PURE__ */ new Map();
19
+ for (let [index, update] of outdated.entries()) {
20
+ let originalFile = update.action.file ?? "unknown file";
21
+ let file = path.relative(path.join(process.cwd(), GITHUB_DIRECTORY), originalFile);
22
+ if (file === "") file = originalFile;
23
+ let group = groups.get(file) ?? [];
24
+ group.push({
25
+ update,
26
+ index
27
+ });
28
+ groups.set(file, group);
29
+ }
30
+ let choices = [];
31
+ let maxActionLength = stripAnsi("Action").length;
32
+ let maxCurrentLength = stripAnsi("Current").length;
33
+ for (let update of outdated) {
34
+ let actionNameRaw = update.action.name;
35
+ let currentRaw = formatVersionOrSha(update.currentVersion);
36
+ maxActionLength = Math.max(maxActionLength, actionNameRaw.length);
37
+ maxCurrentLength = Math.max(maxCurrentLength, currentRaw.length);
38
+ }
39
+ let globalActionWidth = Math.max(maxActionLength, MIN_ACTION_WIDTH);
40
+ let globalCurrentWidth = Math.max(maxCurrentLength, MIN_CURRENT_WIDTH);
41
+ let sortedFiles = [...groups.keys()].toSorted();
42
+ for (let [fileIndex, file] of sortedFiles.entries()) {
43
+ let fileGroup = groups.get(file);
44
+ if (!fileGroup) {
45
+ console.warn(`Unexpected missing group for file: ${file}`);
46
+ continue;
47
+ }
48
+ let tableRows = [];
49
+ let groupOrder = fileGroup;
50
+ tableRows.push({
51
+ current: "Current",
52
+ action: "Action",
53
+ target: "Target",
54
+ arrow: "❯"
55
+ });
56
+ for (let { update } of groupOrder) {
57
+ let hasSha = Boolean(update.latestSha);
58
+ let current = formatVersionOrSha(update.currentVersion);
59
+ let latest = formatVersion(update.latestVersion);
60
+ let actionName = update.action.name;
61
+ if (update.latestSha) {
62
+ let shortSha = update.latestSha.slice(0, 7);
63
+ latest = `${latest} ${pc.gray(`(${shortSha})`)}`;
64
+ }
65
+ if (!hasSha) {
66
+ latest = pc.gray(latest);
67
+ current = pc.gray(current);
68
+ actionName = pc.gray(actionName);
69
+ }
70
+ tableRows.push({
71
+ action: actionName,
72
+ target: latest,
73
+ arrow: "❯",
74
+ current
75
+ });
76
+ }
77
+ let maxActionWidth = Math.max(globalActionWidth, MIN_ACTION_WIDTH);
78
+ let maxCurrentWidth = Math.max(globalCurrentWidth, MIN_CURRENT_WIDTH);
79
+ let groupChildren = [];
80
+ for (let [i, row] of tableRows.entries()) {
81
+ let isHeader = i === 0;
82
+ let formattedRow = formatTableRow(row, maxActionWidth, maxCurrentWidth);
83
+ if (isHeader) groupChildren.push({
84
+ message: pc.gray(` ○ ${formattedRow}`),
85
+ role: "separator",
86
+ indent: "",
87
+ name: ""
88
+ });
89
+ else {
90
+ let entry = groupOrder[i - 1];
91
+ if (!entry) continue;
92
+ let { update, index } = entry;
93
+ let hasSha = Boolean(update.latestSha);
94
+ let enabled = hasSha && !update.isBreaking;
95
+ groupChildren.push({
96
+ message: formattedRow,
97
+ value: String(index),
98
+ name: String(index),
99
+ disabled: !hasSha,
100
+ indent: "",
101
+ enabled
102
+ });
103
+ }
104
+ }
105
+ choices.push({
106
+ message: pc.gray(file),
107
+ value: `label|${file}`,
108
+ choices: groupChildren,
109
+ name: `label|${file}`,
110
+ isGroupLabel: true,
111
+ enabled: false
112
+ });
113
+ if (fileIndex < sortedFiles.length - 1) choices.push({
114
+ role: "separator",
115
+ message: " ",
116
+ name: ""
117
+ });
118
+ }
119
+ try {
120
+ let promptOptions = {
121
+ indicator(_state, choice) {
122
+ let isLabel = Boolean(choice.isGroupLabel);
123
+ if (isLabel) {
124
+ let allChildren = choice.choices ?? [];
125
+ let rows = allChildren.filter((child) => !("role" in child));
126
+ let total = rows.length;
127
+ let selectedCount = rows.filter((row) => Boolean(row.enabled)).length;
128
+ let mark = selectedCount === total ? "●" : "○";
129
+ return ` ${pc.gray(mark)}`;
130
+ }
131
+ return ` ${choice.enabled ? "●" : "○"}`;
132
+ },
133
+ message: `Choose which actions to update (Press ${pc.cyan("<space>")} to select, ${pc.cyan("<a>")} to toggle all, ${pc.cyan("<i>")} to invert selection)`,
134
+ cancel() {
135
+ console.info(pc.yellow("\nSelection cancelled"));
136
+ return null;
137
+ },
138
+ styles: {
139
+ success: pc.reset,
140
+ em: pc.bgBlack,
141
+ dark: pc.reset
142
+ },
143
+ j() {
144
+ return this.down?.() ?? Promise.resolve([]);
145
+ },
146
+ k() {
147
+ return this.up?.() ?? Promise.resolve([]);
148
+ },
149
+ footer: "\nEnter to start updating. Ctrl-c to cancel.",
150
+ type: "multiselect",
151
+ name: "selected",
152
+ pointer: "❯",
153
+ choices
154
+ };
155
+ let { selected } = await enquirer.prompt(promptOptions);
156
+ let selectedIndexes = /* @__PURE__ */ new Set();
157
+ for (let valueString of selected) {
158
+ if (valueString.startsWith("label|")) {
159
+ let fileKey = valueString.slice(6);
160
+ let groupItems = groups.get(fileKey) ?? [];
161
+ for (let { update: upd, index: index$1 } of groupItems) if (upd.latestSha) selectedIndexes.add(index$1);
162
+ continue;
163
+ }
164
+ let index = Number.parseInt(valueString, 10);
165
+ if (Number.isFinite(index)) selectedIndexes.add(index);
166
+ }
167
+ let result = [];
168
+ for (let [index, outdatedUpdate] of outdated.entries()) if (selectedIndexes.has(index) && outdatedUpdate.latestSha) result.push(outdatedUpdate);
169
+ if (result.length === 0) {
170
+ console.info(pc.yellow("\nNo actions selected"));
171
+ return null;
172
+ }
173
+ return result;
174
+ } catch (error) {
175
+ if (error instanceof Error && (error.message.includes("cancelled") || error.message.includes("ESC") || error.name === "ExitPromptError")) {
176
+ console.info(pc.yellow("\nSelection cancelled"));
177
+ return null;
178
+ }
179
+ console.error(pc.red("Unexpected error during selection:"), error);
180
+ throw error;
181
+ }
182
+ }
183
+ function formatTableRow(row, actionWidth, currentWidth) {
184
+ let parts = [
185
+ padString(row.action, actionWidth),
186
+ padString(row.current, currentWidth),
187
+ row.arrow,
188
+ row.target
189
+ ];
190
+ let line = parts.join(" ");
191
+ return line.replace(/\s+$/u, "");
192
+ }
193
+ function isSha(value) {
194
+ if (!value) return false;
195
+ let normalized = value.replace(/^v/u, "");
196
+ return /^[0-9a-f]{7,40}$/iu.test(normalized);
197
+ }
198
+ function formatVersionOrSha(version) {
199
+ if (!version) return pc.gray("unknown");
200
+ if (isSha(version)) return version.slice(0, 7);
201
+ return version;
202
+ }
203
+ export { promptUpdateSelection };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Remove ANSI escape codes from a string.
3
+ *
4
+ * @param string - String with potential ANSI codes.
5
+ * @returns String without ANSI codes.
6
+ */
7
+ export declare function stripAnsi(string: string): string;
@@ -0,0 +1,21 @@
1
+ function stripAnsi(string) {
2
+ let result = "";
3
+ let i = 0;
4
+ while (i < string.length) if (string.charCodeAt(i) === 27 && i + 1 < string.length && string[i + 1] === "[") {
5
+ i += 2;
6
+ while (i < string.length) {
7
+ let char = string[i];
8
+ i++;
9
+ if (char === "m") break;
10
+ if (!/[\d;]/u.test(char)) {
11
+ result += `${String.fromCharCode(27)}[${string.slice(i - 1, i)}`;
12
+ break;
13
+ }
14
+ }
15
+ } else {
16
+ result += string[i];
17
+ i++;
18
+ }
19
+ return result;
20
+ }
21
+ export { stripAnsi };
@@ -0,0 +1,30 @@
1
+ import { GitHubAction } from '../../types/github-action';
2
+ /**
3
+ * Parses a GitHub Action reference string and returns a structured GitHubAction
4
+ * object.
5
+ *
6
+ * @example
7
+ * const action = parseActionReference(
8
+ * 'actions/checkout@v3',
9
+ * 'workflow.yml',
10
+ * 10,
11
+ * )
12
+ * // Returns: {
13
+ * // type: 'external',
14
+ * // name: 'actions/checkout',
15
+ * // version: 'v3',
16
+ * // file: 'workflow.yml',
17
+ * // line: 10,
18
+ * // }
19
+ *
20
+ * @param reference - The action reference string to parse. Can be:
21
+ *
22
+ * - External action: "owner/repo@version" (e.g., "actions/checkout@v3")
23
+ * - Local action: "./path/to/action" or "../path/to/action"
24
+ * - Docker action: "docker://image:tag".
25
+ *
26
+ * @param file - The file path where this action reference was found.
27
+ * @param line - The line number where this action reference was found.
28
+ * @returns A GitHubAction object if parsing succeeds, null otherwise.
29
+ */
30
+ export declare function parseActionReference(reference: string, file: string, line: number): GitHubAction | null;
@@ -0,0 +1,34 @@
1
+ function parseActionReference(reference, file, line) {
2
+ if (!reference || reference.trim() === "") return null;
3
+ if (reference.startsWith("docker://")) return {
4
+ version: void 0,
5
+ name: reference,
6
+ type: "docker",
7
+ file,
8
+ line
9
+ };
10
+ if (reference.startsWith("./") || reference.startsWith("../")) return {
11
+ version: void 0,
12
+ name: reference,
13
+ type: "local",
14
+ file,
15
+ line
16
+ };
17
+ let parts = reference.split("@");
18
+ if (parts.length !== 2) return null;
19
+ let [namePart, version] = parts;
20
+ if (!namePart || !version) return null;
21
+ let segs = namePart.split("/");
22
+ if (segs.length < 2) return null;
23
+ let [owner, repo] = segs;
24
+ if (!owner || !repo) return null;
25
+ for (let seg of segs.slice(2)) if (!seg) return null;
26
+ return {
27
+ type: "external",
28
+ name: namePart,
29
+ version,
30
+ file,
31
+ line
32
+ };
33
+ }
34
+ export { parseActionReference };
@@ -0,0 +1,10 @@
1
+ import { GitHubAction } from '../types/github-action';
2
+ /**
3
+ * Scans a composite GitHub Action file (action.yml or action.yaml) for all
4
+ * action references.
5
+ *
6
+ * @param filePath - The path to the action YAML file to scan.
7
+ * @returns A promise that resolves to an array of GitHubAction objects found in
8
+ * the action file.
9
+ */
10
+ export declare function scanActionFile(filePath: string): Promise<GitHubAction[]>;
@@ -0,0 +1,7 @@
1
+ import { readYamlDocument } from "./fs/read-yaml-document.js";
2
+ import { scanCompositeActionAst } from "./ast/scanners/scan-composite-action-ast.js";
3
+ async function scanActionFile(filePath) {
4
+ let { document, content } = await readYamlDocument(filePath);
5
+ return scanCompositeActionAst(document, content, filePath);
6
+ }
7
+ export { scanActionFile };
@@ -0,0 +1,17 @@
1
+ import { ScanResult } from '../types/scan-result';
2
+ /**
3
+ * Scans a repository for all GitHub Actions usage in workflows and composite
4
+ * actions.
5
+ *
6
+ * @example
7
+ * const result = await scanGitHubActions('/path/to/repo')
8
+ *
9
+ * @param rootPath - The root path of the repository to scan. Defaults to
10
+ * current working directory.
11
+ * @returns A promise that resolves to a ScanResult containing:
12
+ *
13
+ * - Workflows: Map of workflow file paths to their referenced actions
14
+ * - CompositeActions: Map of composite action names to their directory paths
15
+ * - Actions: Flat array of all discovered GitHub Actions.
16
+ */
17
+ export declare function scanGitHubActions(rootPath?: string): Promise<ScanResult>;