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.
- package/bin/actions-up.js +5 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +67 -0
- package/dist/core/api/check-updates.d.ts +10 -0
- package/dist/core/api/check-updates.js +139 -0
- package/dist/core/api/client.d.ts +79 -0
- package/dist/core/api/client.js +187 -0
- package/dist/core/ast/guards/has-range.d.ts +10 -0
- package/dist/core/ast/guards/has-range.js +4 -0
- package/dist/core/ast/guards/is-node.d.ts +8 -0
- package/dist/core/ast/guards/is-node.js +4 -0
- package/dist/core/ast/guards/is-pair.d.ts +8 -0
- package/dist/core/ast/guards/is-pair.js +4 -0
- package/dist/core/ast/guards/is-scalar.d.ts +8 -0
- package/dist/core/ast/guards/is-scalar.js +4 -0
- package/dist/core/ast/guards/is-yaml-map.d.ts +8 -0
- package/dist/core/ast/guards/is-yaml-map.js +4 -0
- package/dist/core/ast/guards/is-yaml-sequence.d.ts +8 -0
- package/dist/core/ast/guards/is-yaml-sequence.js +4 -0
- package/dist/core/ast/scanners/scan-composite-action-ast.d.ts +14 -0
- package/dist/core/ast/scanners/scan-composite-action-ast.js +18 -0
- package/dist/core/ast/scanners/scan-workflow-ast.d.ts +14 -0
- package/dist/core/ast/scanners/scan-workflow-ast.js +23 -0
- package/dist/core/ast/update/apply-updates.d.ts +7 -0
- package/dist/core/ast/update/apply-updates.js +40 -0
- package/dist/core/ast/utils/extract-uses-from-steps.d.ts +13 -0
- package/dist/core/ast/utils/extract-uses-from-steps.js +24 -0
- package/dist/core/ast/utils/find-map-pair.d.ts +12 -0
- package/dist/core/ast/utils/find-map-pair.js +10 -0
- package/dist/core/ast/utils/get-line-number.d.ts +10 -0
- package/dist/core/ast/utils/get-line-number.js +9 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/constants.js +4 -0
- package/dist/core/fs/is-yaml-file.d.ts +7 -0
- package/dist/core/fs/is-yaml-file.js +4 -0
- package/dist/core/fs/read-yaml-document.d.ts +11 -0
- package/dist/core/fs/read-yaml-document.js +11 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/interactive/format-version.d.ts +7 -0
- package/dist/core/interactive/format-version.js +5 -0
- package/dist/core/interactive/pad-string.d.ts +8 -0
- package/dist/core/interactive/pad-string.js +9 -0
- package/dist/core/interactive/prompt-update-selection.d.ts +2 -0
- package/dist/core/interactive/prompt-update-selection.js +203 -0
- package/dist/core/interactive/strip-ansi.d.ts +7 -0
- package/dist/core/interactive/strip-ansi.js +21 -0
- package/dist/core/parsing/parse-action-reference.d.ts +30 -0
- package/dist/core/parsing/parse-action-reference.js +34 -0
- package/dist/core/scan-action-file.d.ts +10 -0
- package/dist/core/scan-action-file.js +7 -0
- package/dist/core/scan-github-actions.d.ts +17 -0
- package/dist/core/scan-github-actions.js +116 -0
- package/dist/core/scan-workflow-file.d.ts +9 -0
- package/dist/core/scan-workflow-file.js +7 -0
- package/dist/core/schema/composite/is-composite-action-runs.d.ts +8 -0
- package/dist/core/schema/composite/is-composite-action-runs.js +6 -0
- package/dist/core/schema/composite/is-composite-action-step.d.ts +8 -0
- package/dist/core/schema/composite/is-composite-action-structure.d.ts +9 -0
- package/dist/core/schema/composite/is-composite-action-structure.js +6 -0
- package/dist/core/schema/workflow/is-workflow-job.d.ts +8 -0
- package/dist/core/schema/workflow/is-workflow-step.d.ts +8 -0
- package/dist/core/schema/workflow/is-workflow-structure.d.ts +8 -0
- package/dist/core/schema/workflow/is-workflow-structure.js +6 -0
- package/dist/package.js +2 -0
- package/dist/types/action-update.d.ts +21 -0
- package/dist/types/composite-action-runs.d.ts +12 -0
- package/dist/types/composite-action-step.d.ts +23 -0
- package/dist/types/composite-action-structure.d.ts +21 -0
- package/dist/types/github-action.d.ts +23 -0
- package/dist/types/scan-result.d.ts +12 -0
- package/dist/types/workflow-job.d.ts +18 -0
- package/dist/types/workflow-step.d.ts +20 -0
- package/dist/types/workflow-structure.d.ts +15 -0
- package/license.md +20 -0
- package/package.json +52 -1
- 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,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,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,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,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,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>;
|