actions-up 1.10.1 → 1.11.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/dist/cli/index.js +82 -57
- package/dist/core/ast/scanners/scan-composite-action-ast.js +2 -2
- package/dist/core/ast/scanners/scan-workflow-ast.js +7 -7
- package/dist/core/fs/find-yaml-files-recursive.d.ts +7 -0
- package/dist/core/fs/find-yaml-files-recursive.js +20 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -1
- package/dist/core/interactive/prompt-update-selection.js +9 -9
- package/dist/core/scan-action-file.js +1 -1
- package/dist/core/scan-github-actions.js +68 -68
- package/dist/core/scan-recursive.d.ts +10 -0
- package/dist/core/scan-recursive.js +48 -0
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/readme.md +12 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,87 +1,95 @@
|
|
|
1
1
|
import { readInlineVersionComment } from "../core/versions/read-inline-version-comment.js";
|
|
2
|
+
import { GITHUB_DIRECTORY } from "../core/constants.js";
|
|
2
3
|
import { isSha } from "../core/versions/is-sha.js";
|
|
3
4
|
import { promptUpdateSelection } from "../core/interactive/prompt-update-selection.js";
|
|
4
5
|
import { getUpdateLevel } from "../core/versions/get-update-level.js";
|
|
5
6
|
import { applyUpdates } from "../core/ast/update/apply-updates.js";
|
|
6
7
|
import { shouldIgnore } from "../core/ignore/should-ignore.js";
|
|
7
8
|
import { checkUpdates } from "../core/api/check-updates.js";
|
|
9
|
+
import { scanRecursive } from "../core/scan-recursive.js";
|
|
8
10
|
import { scanGitHubActions } from "../core/scan-github-actions.js";
|
|
9
11
|
import "../core/index.js";
|
|
10
12
|
import { version } from "../package.js";
|
|
13
|
+
import { relative, resolve } from "node:path";
|
|
11
14
|
import { createSpinner } from "nanospinner";
|
|
12
15
|
import "node:worker_threads";
|
|
13
16
|
import pc from "picocolors";
|
|
14
17
|
import cac from "cac";
|
|
15
18
|
function run() {
|
|
16
|
-
let
|
|
17
|
-
|
|
19
|
+
let b = cac("actions-up");
|
|
20
|
+
b.help().version(version).option("--dir <directory>", "Custom directory name (default: .github)").option("--dry-run", "Preview changes without applying them").option("--exclude <regex>", "Exclude actions by regex (repeatable)").option("--include-branches", "Also check actions pinned to branches (default: false)").option("--min-age <days>", "Minimum age in days for updates (default: 0)", { default: 0 }).option("--mode <mode>", "Update mode: major, minor, or patch (default: major)", { default: "major" }).option("--recursive, -r", "Recursively scan directories for YAML files").option("--yes, -y", "Skip all confirmations").command("", "Update GitHub Actions").action(async (g) => {
|
|
18
21
|
console.info(pc.cyan("\n🚀 Actions Up!\n"));
|
|
19
|
-
let
|
|
22
|
+
let y = createSpinner("Scanning GitHub Actions...").start(), b = [];
|
|
23
|
+
Array.isArray(g.dir) ? b.push(...g.dir) : typeof g.dir == "string" ? b.push(g.dir) : b.push(GITHUB_DIRECTORY);
|
|
24
|
+
let x = [...new Set(b.map((e) => {
|
|
25
|
+
let d = process.cwd();
|
|
26
|
+
return relative(d, resolve(d, e)) || ".";
|
|
27
|
+
}))];
|
|
20
28
|
try {
|
|
21
|
-
let
|
|
22
|
-
if (
|
|
29
|
+
let d = mergeScanResults(g.recursive ? await Promise.all(x.map((e) => scanRecursive(process.cwd(), e))) : await Promise.all(x.map((e) => scanGitHubActions(process.cwd(), e)))), _ = d.actions.length, v = d.workflows.size, b = d.compositeActions.size;
|
|
30
|
+
if (y.success(`Found ${pc.yellow(_)} actions in ${pc.yellow(v)} workflows and ${pc.yellow(b)} composite actions`), _ === 0) {
|
|
23
31
|
console.info(pc.green("\n✨ No GitHub Actions found in this repository"));
|
|
24
32
|
return;
|
|
25
33
|
}
|
|
26
|
-
let
|
|
27
|
-
Array.isArray(
|
|
28
|
-
let
|
|
29
|
-
if (
|
|
30
|
-
let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"),
|
|
31
|
-
|
|
32
|
-
let { name:
|
|
33
|
-
for (let e of
|
|
34
|
+
let S = d.actions, C = [];
|
|
35
|
+
Array.isArray(g.exclude) ? C.push(...g.exclude) : typeof g.exclude == "string" && C.push(g.exclude);
|
|
36
|
+
let w = C.flatMap((e) => e.split(",")).map((e) => e.trim()).filter(Boolean);
|
|
37
|
+
if (w.length > 0) {
|
|
38
|
+
let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"), d = e(w);
|
|
39
|
+
d.length > 0 && (S = S.filter((e) => {
|
|
40
|
+
let { name: f } = e;
|
|
41
|
+
for (let e of d) if (e.test(f)) return !1;
|
|
34
42
|
return !0;
|
|
35
43
|
}));
|
|
36
44
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
45
|
+
if (y = createSpinner("Checking for updates...").start(), S.length === 0) {
|
|
46
|
+
y.success("No actions to check after excludes"), console.info(pc.green("\n✨ Nothing to check after excludes\n"));
|
|
39
47
|
return;
|
|
40
48
|
}
|
|
41
|
-
let
|
|
42
|
-
await Promise.all(
|
|
43
|
-
await shouldIgnore(e.action.file, e.action.line) ||
|
|
49
|
+
let T = g.includeBranches ?? !1, E = await checkUpdates(S, process.env.GITHUB_TOKEN, { includeBranches: T }), D = [];
|
|
50
|
+
await Promise.all(E.map(async (e) => {
|
|
51
|
+
await shouldIgnore(e.action.file, e.action.line) || D.push(e);
|
|
44
52
|
}));
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
if (
|
|
49
|
-
let
|
|
50
|
-
let
|
|
51
|
-
if (isSha(
|
|
52
|
-
let
|
|
53
|
-
|
|
53
|
+
let O = D.filter((e) => e.status === "skipped"), k = D.filter((e) => e.hasUpdate), A = g.minAge * 24 * 60 * 60 * 1e3, j = Date.now();
|
|
54
|
+
k = k.filter((e) => e.publishedAt ? j - e.publishedAt.getTime() >= A : !0);
|
|
55
|
+
let M = normalizeUpdateMode(g.mode), N = [];
|
|
56
|
+
if (M !== "major") {
|
|
57
|
+
let d = /* @__PURE__ */ new Map(), p = await Promise.all(k.map(async (p) => {
|
|
58
|
+
let m = p.currentVersion;
|
|
59
|
+
if (isSha(p.currentVersion)) {
|
|
60
|
+
let f = await readInlineVersionComment(p.action.file, p.action.line, d);
|
|
61
|
+
f && (m = f);
|
|
54
62
|
}
|
|
55
|
-
let
|
|
63
|
+
let h = getUpdateLevel(m, p.latestVersion);
|
|
56
64
|
return {
|
|
57
|
-
allowed:
|
|
58
|
-
update:
|
|
65
|
+
allowed: M === "minor" ? h === "minor" || h === "patch" || h === "none" : h === "patch" || h === "none",
|
|
66
|
+
update: p
|
|
59
67
|
};
|
|
60
|
-
})),
|
|
61
|
-
for (let e of
|
|
62
|
-
|
|
68
|
+
})), m = [];
|
|
69
|
+
for (let e of p) e.allowed ? m.push(e.update) : N.push(e.update);
|
|
70
|
+
k = m;
|
|
63
71
|
}
|
|
64
|
-
let
|
|
65
|
-
if (
|
|
66
|
-
|
|
72
|
+
let P = k.filter((e) => e.isBreaking);
|
|
73
|
+
if (k.length === 0) {
|
|
74
|
+
y.success("All actions are up to date!"), O.length > 0 && printSkippedWarning(O, T), N.length > 0 && printModeWarning(N, M), console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
|
|
67
75
|
return;
|
|
68
76
|
}
|
|
69
|
-
if (
|
|
77
|
+
if (y.success(`Found ${pc.yellow(k.length)} updates available${P.length > 0 ? ` (${pc.redBright(P.length)} breaking)` : ""}`), O.length > 0 && printSkippedWarning(O, T), N.length > 0 && printModeWarning(N, M), g.dryRun) {
|
|
70
78
|
console.info(pc.yellow("\n📋 Dry Run - No changes will be made\n"));
|
|
71
|
-
for (let e of
|
|
72
|
-
console.info(pc.gray(`\n${
|
|
79
|
+
for (let e of k) console.info(`${pc.cyan(e.action.file ?? "unknown")}:\n${e.action.name}: ${pc.redBright(e.currentVersion)} → ${pc.green(e.latestVersion)} ${e.latestSha ? pc.gray(`(${e.latestSha.slice(0, 7)})`) : ""}\n`);
|
|
80
|
+
console.info(pc.gray(`\n${k.length} actions would be updated\n`));
|
|
73
81
|
return;
|
|
74
82
|
}
|
|
75
|
-
if (
|
|
76
|
-
let e =
|
|
83
|
+
if (g.yes) {
|
|
84
|
+
let e = k.filter((e) => e.latestSha);
|
|
77
85
|
if (e.length === 0) {
|
|
78
86
|
console.info(pc.yellow("\n⚠️ No actions with SHA available for update\n"));
|
|
79
87
|
return;
|
|
80
88
|
}
|
|
81
89
|
console.info(pc.yellow(`\n🔄 Updating ${e.length} actions...\n`)), await applyUpdates(e), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
82
90
|
} else {
|
|
83
|
-
(
|
|
84
|
-
let e = await promptUpdateSelection(
|
|
91
|
+
(O.length > 0 || N.length > 0) && console.info("");
|
|
92
|
+
let e = await promptUpdateSelection(k, { showAge: g.minAge > 0 });
|
|
85
93
|
if (!e || e.length === 0) {
|
|
86
94
|
console.info(pc.gray("\nNo updates applied"));
|
|
87
95
|
return;
|
|
@@ -89,30 +97,47 @@ function run() {
|
|
|
89
97
|
console.info(pc.yellow(`\n🔄 Updating ${e.length} selected actions...\n`)), await applyUpdates(e), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
90
98
|
}
|
|
91
99
|
} catch (e) {
|
|
92
|
-
|
|
100
|
+
y.error("Failed"), e instanceof Error && e.name === "GitHubRateLimitError" ? (console.error(pc.yellow("\n⚠️ Rate Limit Exceeded\n")), console.error(e.message), console.error(pc.gray("\nExample: GITHUB_TOKEN=ghp_xxxx actions-up\n"))) : console.error(pc.redBright("\nError:"), e instanceof Error ? e.message : String(e)), process.exit(1);
|
|
93
101
|
}
|
|
94
|
-
}),
|
|
102
|
+
}), b.parse();
|
|
95
103
|
}
|
|
96
|
-
function
|
|
104
|
+
function mergeScanResults(e) {
|
|
105
|
+
let d = {
|
|
106
|
+
compositeActions: /* @__PURE__ */ new Map(),
|
|
107
|
+
workflows: /* @__PURE__ */ new Map(),
|
|
108
|
+
actions: []
|
|
109
|
+
};
|
|
110
|
+
for (let f of e) {
|
|
111
|
+
for (let [e, p] of f.workflows) d.workflows.set(e, p);
|
|
112
|
+
for (let [, e] of f.compositeActions) d.compositeActions.set(e, e);
|
|
113
|
+
d.actions.push(...f.actions);
|
|
114
|
+
}
|
|
115
|
+
let f = /* @__PURE__ */ new Set();
|
|
116
|
+
return d.actions = d.actions.filter((e) => {
|
|
117
|
+
let d = `${e.file}:${e.line}:${e.name}:${e.version}`;
|
|
118
|
+
return f.has(d) ? !1 : (f.add(d), !0);
|
|
119
|
+
}), d;
|
|
120
|
+
}
|
|
121
|
+
function printModeWarning(e, d) {
|
|
97
122
|
if (e.length === 0) return;
|
|
98
|
-
let
|
|
99
|
-
console.info(pc.yellow(`\n⚠️ Skipped ${e.length} ${
|
|
100
|
-
for (let
|
|
101
|
-
let e =
|
|
123
|
+
let f = new Intl.PluralRules("en-US", { type: "cardinal" }).select(e.length) === "one" ? "action" : "actions", p = d === "minor" ? "major" : "major/minor";
|
|
124
|
+
console.info(pc.yellow(`\n⚠️ Skipped ${e.length} ${f} due to ${p} updates`));
|
|
125
|
+
for (let d of e) {
|
|
126
|
+
let e = d.action.uses ?? `${d.action.name}@${d.currentVersion ?? "unknown"}`;
|
|
102
127
|
console.info(pc.gray(` • ${e}`));
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
|
-
function printSkippedWarning(e,
|
|
106
|
-
let
|
|
107
|
-
console.info(pc.yellow(`\n⚠️ Skipped ${e.length} ${
|
|
108
|
-
for (let
|
|
109
|
-
let e =
|
|
130
|
+
function printSkippedWarning(e, d) {
|
|
131
|
+
let f = new Intl.PluralRules("en-US", { type: "cardinal" }).select(e.length) === "one" ? "action" : "actions", p = d ? "" : " (use --include-branches to check them)";
|
|
132
|
+
console.info(pc.yellow(`\n⚠️ Skipped ${e.length} ${f} pinned to branches${p}`));
|
|
133
|
+
for (let d of e) {
|
|
134
|
+
let e = d.action.uses ?? `${d.action.name}@${d.currentVersion ?? "unknown"}`;
|
|
110
135
|
console.info(pc.gray(` • ${e}`));
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
138
|
function normalizeUpdateMode(e) {
|
|
114
|
-
let
|
|
115
|
-
if (
|
|
139
|
+
let d = (e ?? "major").toLowerCase();
|
|
140
|
+
if (d === "major" || d === "minor" || d === "patch") return d;
|
|
116
141
|
throw Error(`Invalid mode "${e}". Expected "major", "minor", or "patch".`);
|
|
117
142
|
}
|
|
118
143
|
export { run };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { isCompositeActionStructure } from "../../schema/composite/is-composite-action-structure.js";
|
|
2
|
+
import { isCompositeActionRuns } from "../../schema/composite/is-composite-action-runs.js";
|
|
1
3
|
import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
2
4
|
import { extractUsesFromSteps } from "../utils/extract-uses-from-steps.js";
|
|
3
5
|
import { findMapPair } from "../utils/find-map-pair.js";
|
|
4
|
-
import { isCompositeActionStructure } from "../../schema/composite/is-composite-action-structure.js";
|
|
5
|
-
import { isCompositeActionRuns } from "../../schema/composite/is-composite-action-runs.js";
|
|
6
6
|
function scanCompositeActionAst(a, o, s) {
|
|
7
7
|
let c = a.toJSON();
|
|
8
8
|
if (!isCompositeActionStructure(c) || !a.contents || !isYAMLMap(a.contents)) return [];
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isWorkflowStructure } from "../../schema/workflow/is-workflow-structure.js";
|
|
2
1
|
import { parseActionReference } from "../../parsing/parse-action-reference.js";
|
|
3
2
|
import { getLineNumberForKey } from "../utils/get-line-number.js";
|
|
4
3
|
import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
@@ -7,19 +6,20 @@ import { isNode } from "../guards/is-node.js";
|
|
|
7
6
|
import { isPair } from "../guards/is-pair.js";
|
|
8
7
|
import { extractUsesFromSteps } from "../utils/extract-uses-from-steps.js";
|
|
9
8
|
import { findMapPair } from "../utils/find-map-pair.js";
|
|
9
|
+
import { isWorkflowStructure } from "../../schema/workflow/is-workflow-structure.js";
|
|
10
10
|
function scanWorkflowAst(l, u, d) {
|
|
11
11
|
if (!isWorkflowStructure(l.toJSON()) || !l.contents || !isYAMLMap(l.contents)) return [];
|
|
12
12
|
let f = findMapPair(l.contents, "jobs");
|
|
13
13
|
if (!f?.value || !isYAMLMap(f.value)) return [];
|
|
14
14
|
let p = [];
|
|
15
|
-
for (let
|
|
16
|
-
if (!isPair(
|
|
17
|
-
let l = isScalar(
|
|
15
|
+
for (let c of f.value.items) {
|
|
16
|
+
if (!isPair(c) || !c.value || !isNode(c.value) || !isYAMLMap(c.value)) continue;
|
|
17
|
+
let l = isScalar(c.key) ? String(c.key.value) : void 0, f = findMapPair(c.value, "uses");
|
|
18
18
|
if (f?.value && f.key && isScalar(f.value)) {
|
|
19
|
-
let
|
|
20
|
-
|
|
19
|
+
let s = parseActionReference(String(f.value.value), d, getLineNumberForKey(u, f.key));
|
|
20
|
+
s && (l && (s.job = l), p.push(s));
|
|
21
21
|
}
|
|
22
|
-
let m = findMapPair(
|
|
22
|
+
let m = findMapPair(c.value, "steps");
|
|
23
23
|
m?.value && p.push(...extractUsesFromSteps({
|
|
24
24
|
stepsNode: m.value,
|
|
25
25
|
filePath: d,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively finds all YAML files in a directory.
|
|
3
|
+
*
|
|
4
|
+
* @param directory - The absolute path to the directory to search.
|
|
5
|
+
* @returns A promise that resolves to an array of absolute paths to YAML files.
|
|
6
|
+
*/
|
|
7
|
+
export declare function findYamlFilesRecursive(directory: string): Promise<string[]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isYamlFile } from "./is-yaml-file.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
4
|
+
async function findYamlFilesRecursive(i) {
|
|
5
|
+
let a = [], o = /* @__PURE__ */ new Set();
|
|
6
|
+
async function s(i) {
|
|
7
|
+
if ((await lstat(i)).isSymbolicLink() || o.has(i)) return;
|
|
8
|
+
o.add(i);
|
|
9
|
+
let c = (await readdir(i)).map(async (r) => {
|
|
10
|
+
try {
|
|
11
|
+
let o = join(i, r), c = await lstat(o);
|
|
12
|
+
if (c.isSymbolicLink()) return;
|
|
13
|
+
c.isDirectory() ? await s(o) : c.isFile() && isYamlFile(r) && a.push(o);
|
|
14
|
+
} catch {}
|
|
15
|
+
});
|
|
16
|
+
await Promise.all(c);
|
|
17
|
+
}
|
|
18
|
+
return await s(i), a;
|
|
19
|
+
}
|
|
20
|
+
export { findYamlFilesRecursive };
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyUpdates } from "./ast/update/apply-updates.js";
|
|
2
2
|
import { checkUpdates } from "./api/check-updates.js";
|
|
3
|
+
import { scanRecursive } from "./scan-recursive.js";
|
|
3
4
|
import { scanGitHubActions } from "./scan-github-actions.js";
|
|
4
|
-
export { applyUpdates, checkUpdates, scanGitHubActions };
|
|
5
|
+
export { applyUpdates, checkUpdates, scanGitHubActions, scanRecursive };
|
|
@@ -4,10 +4,10 @@ import { GITHUB_DIRECTORY } from "../constants.js";
|
|
|
4
4
|
import { isSha } from "../versions/is-sha.js";
|
|
5
5
|
import { stripAnsi } from "./strip-ansi.js";
|
|
6
6
|
import { padString } from "./pad-string.js";
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import "node:worker_threads";
|
|
8
9
|
import pc from "picocolors";
|
|
9
10
|
import enquirer from "enquirer";
|
|
10
|
-
import path from "node:path";
|
|
11
11
|
var MIN_ACTION_WIDTH = 40, MIN_JOB_WIDTH = 4, MIN_CURRENT_WIDTH = 16, MAX_VERSION_WIDTH = 7;
|
|
12
12
|
async function promptUpdateSelection(g, v = {}) {
|
|
13
13
|
let { showAge: y = !1 } = v;
|
|
@@ -41,8 +41,8 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
41
41
|
};
|
|
42
42
|
})), C = [], w = stripAnsi("Action").length, T = stripAnsi("Current").length, E = stripAnsi("Job").length, D = 0, O = !1;
|
|
43
43
|
for (let [t, a] of b.entries()) {
|
|
44
|
-
let o = a.action.name,
|
|
45
|
-
if (w = Math.max(w, o.length), T = Math.max(T, stripAnsi(d).length,
|
|
44
|
+
let o = a.action.name, l = S[t], d = l.display, f = a.action.job ?? "–";
|
|
45
|
+
if (w = Math.max(w, o.length), T = Math.max(T, stripAnsi(d).length, l.versionForPadding && l.shortSha ? stripAnsi(`${padString(l.versionForPadding, D + 1)}${pc.gray(`(${l.shortSha})`)}`).length : 0), E = Math.max(E, f.length), a.latestVersion) {
|
|
46
46
|
let o = formatVersion(a.latestVersion, S[t]?.effectiveForDiff ?? a.currentVersion);
|
|
47
47
|
D = Math.max(D, stripAnsi(o).length);
|
|
48
48
|
}
|
|
@@ -56,7 +56,7 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
56
56
|
console.warn(`Unexpected missing group for file: ${a}`);
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
|
-
let s = [],
|
|
59
|
+
let s = [], l = o;
|
|
60
60
|
s.push({
|
|
61
61
|
current: "Current",
|
|
62
62
|
action: "Action",
|
|
@@ -65,10 +65,10 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
65
65
|
job: "Job",
|
|
66
66
|
age: "Age"
|
|
67
67
|
});
|
|
68
|
-
for (let { update: t, index: a } of
|
|
69
|
-
let o = !!t.latestSha,
|
|
70
|
-
|
|
71
|
-
let f =
|
|
68
|
+
for (let { update: t, index: a } of l) {
|
|
69
|
+
let o = !!t.latestSha, l = S[a], d = l.display;
|
|
70
|
+
l.versionForPadding && l.shortSha && (d = `${padString(l.versionForPadding, M + 1)}${pc.gray(`(${l.shortSha})`)}`);
|
|
71
|
+
let f = l.effectiveForDiff ?? t.currentVersion, p = formatVersion(t.latestVersion, f), m = t.action.name;
|
|
72
72
|
if (t.latestSha) {
|
|
73
73
|
let i = t.latestSha.slice(0, 7);
|
|
74
74
|
p = `${padString(p, M + 1)}${pc.gray(`(${i})`)}`;
|
|
@@ -101,7 +101,7 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
101
101
|
name: ""
|
|
102
102
|
});
|
|
103
103
|
else {
|
|
104
|
-
let { update: i, index: a } =
|
|
104
|
+
let { update: i, index: a } = l[t - 1], s = !!i.latestSha, c = s && !i.isBreaking;
|
|
105
105
|
_.push({
|
|
106
106
|
message: o,
|
|
107
107
|
value: String(a),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readYamlDocument } from "./fs/read-yaml-document.js";
|
|
2
1
|
import { scanCompositeActionAst } from "./ast/scanners/scan-composite-action-ast.js";
|
|
2
|
+
import { readYamlDocument } from "./fs/read-yaml-document.js";
|
|
3
3
|
async function scanActionFile(n) {
|
|
4
4
|
let { document: r, content: i } = await readYamlDocument(n);
|
|
5
5
|
return scanCompositeActionAst(r, i, n);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { ACTIONS_DIRECTORY, GITHUB_DIRECTORY, WORKFLOWS_DIRECTORY } from "./constants.js";
|
|
2
|
+
import { isYamlFile } from "./fs/is-yaml-file.js";
|
|
2
3
|
import { scanWorkflowFile } from "./scan-workflow-file.js";
|
|
3
4
|
import { scanActionFile } from "./scan-action-file.js";
|
|
4
|
-
import { isYamlFile } from "./fs/is-yaml-file.js";
|
|
5
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
5
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
-
|
|
6
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
|
+
async function scanGitHubActions(p = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
8
8
|
let h = {
|
|
9
9
|
compositeActions: /* @__PURE__ */ new Map(),
|
|
10
10
|
workflows: /* @__PURE__ */ new Map(),
|
|
11
11
|
actions: []
|
|
12
|
-
}, g = resolve(
|
|
13
|
-
function _(e,
|
|
14
|
-
let
|
|
15
|
-
return
|
|
12
|
+
}, g = resolve(p);
|
|
13
|
+
function _(e, a) {
|
|
14
|
+
let o = relative(e, a);
|
|
15
|
+
return o !== "" && !o.startsWith("..") && !isAbsolute(o);
|
|
16
16
|
}
|
|
17
17
|
let v = join(g, m);
|
|
18
18
|
if (!_(g, v)) throw Error("Invalid path: detected path traversal attempt");
|
|
@@ -21,8 +21,8 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
21
21
|
}
|
|
22
22
|
async function b(e) {
|
|
23
23
|
try {
|
|
24
|
-
let
|
|
25
|
-
return typeof
|
|
24
|
+
let a = await stat(e);
|
|
25
|
+
return typeof a.isFile == "function" ? a.isFile() : !1;
|
|
26
26
|
} catch {
|
|
27
27
|
return !1;
|
|
28
28
|
}
|
|
@@ -31,13 +31,13 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
31
31
|
try {
|
|
32
32
|
if ((await stat(x)).isDirectory()) {
|
|
33
33
|
let e = (await readdir(x)).filter((e) => y(e) ? isYamlFile(e) : !1).map(async (e) => {
|
|
34
|
-
let
|
|
34
|
+
let a = join(x, e);
|
|
35
35
|
try {
|
|
36
|
-
let
|
|
36
|
+
let s = await scanWorkflowFile(a);
|
|
37
37
|
return {
|
|
38
38
|
path: `${m}/${WORKFLOWS_DIRECTORY}/${e}`,
|
|
39
39
|
success: !0,
|
|
40
|
-
actions:
|
|
40
|
+
actions: s
|
|
41
41
|
};
|
|
42
42
|
} catch {
|
|
43
43
|
return {
|
|
@@ -46,111 +46,111 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
46
46
|
actions: []
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
}),
|
|
50
|
-
for (let e of
|
|
49
|
+
}), a = await Promise.all(e);
|
|
50
|
+
for (let e of a) e.success && e.path && (e.actions.length > 0 ? (h.workflows.set(e.path, e.actions), h.actions.push(...e.actions)) : h.workflows.set(e.path, []));
|
|
51
51
|
}
|
|
52
52
|
} catch {}
|
|
53
53
|
try {
|
|
54
|
-
let e = join(g, "action.yml"),
|
|
54
|
+
let e = join(g, "action.yml"), a = join(g, "action.yaml"), o = null, s = [];
|
|
55
55
|
if (await b(e)) try {
|
|
56
|
-
|
|
56
|
+
s = await scanActionFile(e), o = e;
|
|
57
57
|
} catch {
|
|
58
|
-
|
|
58
|
+
o = null;
|
|
59
59
|
}
|
|
60
|
-
if (!
|
|
61
|
-
|
|
60
|
+
if (!o && await b(a)) try {
|
|
61
|
+
s = await scanActionFile(a), o = a;
|
|
62
62
|
} catch {
|
|
63
|
-
|
|
63
|
+
o = null;
|
|
64
64
|
}
|
|
65
|
-
if (
|
|
66
|
-
let e = relative(g,
|
|
67
|
-
h.compositeActions.set(e, e),
|
|
65
|
+
if (o) {
|
|
66
|
+
let e = relative(g, o);
|
|
67
|
+
h.compositeActions.set(e, e), s.length > 0 && h.actions.push(...s);
|
|
68
68
|
}
|
|
69
69
|
} catch {}
|
|
70
70
|
let S = join(v, ACTIONS_DIRECTORY);
|
|
71
71
|
try {
|
|
72
72
|
if ((await stat(S)).isDirectory()) {
|
|
73
|
-
let
|
|
74
|
-
if (!y(
|
|
75
|
-
let
|
|
73
|
+
let a = (await readdir(S)).map(async (a) => {
|
|
74
|
+
if (!y(a)) return null;
|
|
75
|
+
let o = join(S, a);
|
|
76
76
|
try {
|
|
77
|
-
if (!(await stat(
|
|
78
|
-
let
|
|
77
|
+
if (!(await stat(o)).isDirectory()) return null;
|
|
78
|
+
let s = join(o, "action.yml"), c = [];
|
|
79
79
|
try {
|
|
80
|
-
|
|
80
|
+
c = await scanActionFile(s);
|
|
81
81
|
} catch {
|
|
82
82
|
try {
|
|
83
|
-
|
|
83
|
+
s = join(o, "action.yaml"), c = await scanActionFile(s);
|
|
84
84
|
} catch {
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
return {
|
|
89
|
-
path: `${m}/${ACTIONS_DIRECTORY}/${
|
|
90
|
-
name:
|
|
91
|
-
actions:
|
|
89
|
+
path: `${m}/${ACTIONS_DIRECTORY}/${a}`,
|
|
90
|
+
name: a,
|
|
91
|
+
actions: c
|
|
92
92
|
};
|
|
93
93
|
} catch {
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
|
-
}),
|
|
97
|
-
for (let e of
|
|
96
|
+
}), o = await Promise.all(a);
|
|
97
|
+
for (let e of o) e && (h.compositeActions.set(e.name, e.path), h.actions.push(...e.actions));
|
|
98
98
|
}
|
|
99
99
|
} catch {}
|
|
100
100
|
try {
|
|
101
101
|
let e = await getCurrentRepoSlug(g);
|
|
102
102
|
if (e) {
|
|
103
103
|
if (process.env.ACTIONS_UP_TEST_THROW === "1") throw Error("test");
|
|
104
|
-
let
|
|
105
|
-
for (let
|
|
106
|
-
if (
|
|
107
|
-
let
|
|
108
|
-
if (
|
|
109
|
-
let
|
|
110
|
-
_(g,
|
|
104
|
+
let a = /* @__PURE__ */ new Set(), o = [];
|
|
105
|
+
for (let s of h.actions) {
|
|
106
|
+
if (s.type !== "external") continue;
|
|
107
|
+
let c = s.name.split("/");
|
|
108
|
+
if (c.length < 3 || `${c[0]}/${c[1]}` !== e) continue;
|
|
109
|
+
let l = join(g, ...c.slice(2));
|
|
110
|
+
_(g, l) && (a.has(l) || (a.add(l), o.push(l)));
|
|
111
111
|
}
|
|
112
|
-
async function
|
|
113
|
-
if (
|
|
114
|
-
let
|
|
112
|
+
async function s() {
|
|
113
|
+
if (o.length === 0) return;
|
|
114
|
+
let c = o.splice(0), u = await Promise.all(c.map(async (o) => {
|
|
115
115
|
try {
|
|
116
|
-
let
|
|
116
|
+
let s = join(o, "action.yml"), c = join(o, "action.yaml"), u = s;
|
|
117
117
|
try {
|
|
118
|
-
if (!(await stat(
|
|
118
|
+
if (!(await stat(s)).isFile()) throw Error("not a file");
|
|
119
119
|
} catch {
|
|
120
|
-
if (!(await stat(
|
|
121
|
-
|
|
120
|
+
if (!(await stat(c)).isFile()) throw Error("not a file");
|
|
121
|
+
u = c;
|
|
122
122
|
}
|
|
123
|
-
let
|
|
124
|
-
|
|
125
|
-
let
|
|
126
|
-
for (let
|
|
127
|
-
if (
|
|
128
|
-
let
|
|
129
|
-
if (
|
|
130
|
-
let
|
|
131
|
-
_(g,
|
|
123
|
+
let d = await scanActionFile(u);
|
|
124
|
+
d.length > 0 && h.actions.push(...d);
|
|
125
|
+
let f = [];
|
|
126
|
+
for (let o of d) {
|
|
127
|
+
if (o.type !== "external") continue;
|
|
128
|
+
let s = o.name.split("/");
|
|
129
|
+
if (s.length < 3 || `${s[0]}/${s[1]}` !== e) continue;
|
|
130
|
+
let c = join(g, ...s.slice(2));
|
|
131
|
+
_(g, c) && (a.has(c) || (a.add(c), f.push(c)));
|
|
132
132
|
}
|
|
133
|
-
return
|
|
133
|
+
return f;
|
|
134
134
|
} catch {
|
|
135
135
|
return [];
|
|
136
136
|
}
|
|
137
137
|
}));
|
|
138
|
-
for (let e of
|
|
139
|
-
await
|
|
138
|
+
for (let e of u) for (let a of e) o.push(a);
|
|
139
|
+
await s();
|
|
140
140
|
}
|
|
141
|
-
await
|
|
141
|
+
await s();
|
|
142
142
|
}
|
|
143
143
|
} catch {}
|
|
144
144
|
return h;
|
|
145
145
|
}
|
|
146
146
|
async function getCurrentRepoSlug(e) {
|
|
147
|
-
let
|
|
148
|
-
if (
|
|
147
|
+
let a = process.env.GITHUB_REPOSITORY;
|
|
148
|
+
if (a && /^[^\s/]+\/[^\s/]+$/u.test(a)) return a;
|
|
149
149
|
try {
|
|
150
|
-
let
|
|
151
|
-
if (
|
|
152
|
-
let
|
|
153
|
-
if (
|
|
150
|
+
let a = await readFile(join(e, ".git", "config"), "utf8"), o = a.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim();
|
|
151
|
+
if (o ||= a.match(/url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim(), !o) return null;
|
|
152
|
+
let s = o.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^./]+)(?:\.git)?$/u);
|
|
153
|
+
if (s?.groups) return `${s.groups.owner}/${s.groups.repo}`;
|
|
154
154
|
} catch {}
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ScanResult } from '../types/scan-result';
|
|
2
|
+
/**
|
|
3
|
+
* Recursively scans a directory for all YAML files and classifies them as
|
|
4
|
+
* workflows or composite actions.
|
|
5
|
+
*
|
|
6
|
+
* @param rootPath - The root path of the repository.
|
|
7
|
+
* @param directory - The directory to scan recursively, relative to rootPath.
|
|
8
|
+
* @returns A promise that resolves to a ScanResult.
|
|
9
|
+
*/
|
|
10
|
+
export declare function scanRecursive(rootPath: string, directory: string): Promise<ScanResult>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isCompositeActionStructure } from "./schema/composite/is-composite-action-structure.js";
|
|
2
|
+
import { scanCompositeActionAst } from "./ast/scanners/scan-composite-action-ast.js";
|
|
3
|
+
import { isWorkflowStructure } from "./schema/workflow/is-workflow-structure.js";
|
|
4
|
+
import { findYamlFilesRecursive } from "./fs/find-yaml-files-recursive.js";
|
|
5
|
+
import { scanWorkflowAst } from "./ast/scanners/scan-workflow-ast.js";
|
|
6
|
+
import { readYamlDocument } from "./fs/read-yaml-document.js";
|
|
7
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
8
|
+
async function scanRecursive(d, f) {
|
|
9
|
+
let p = {
|
|
10
|
+
compositeActions: /* @__PURE__ */ new Map(),
|
|
11
|
+
workflows: /* @__PURE__ */ new Map(),
|
|
12
|
+
actions: []
|
|
13
|
+
}, m = resolve(d), h = resolve(m, f), g = relative(m, h);
|
|
14
|
+
if (g !== "" && (g.startsWith("..") || isAbsolute(g))) throw Error("Invalid path: detected path traversal attempt");
|
|
15
|
+
let _;
|
|
16
|
+
try {
|
|
17
|
+
_ = await findYamlFilesRecursive(h);
|
|
18
|
+
} catch {
|
|
19
|
+
return p;
|
|
20
|
+
}
|
|
21
|
+
let v = _.map(async (o) => {
|
|
22
|
+
let s = relative(m, o);
|
|
23
|
+
try {
|
|
24
|
+
let { document: c, content: l } = await readYamlDocument(o), u = c.toJSON();
|
|
25
|
+
if (isWorkflowStructure(u) && hasKey(u, "jobs")) return {
|
|
26
|
+
type: "workflow",
|
|
27
|
+
path: s,
|
|
28
|
+
actions: scanWorkflowAst(c, l, o)
|
|
29
|
+
};
|
|
30
|
+
if (isCompositeActionStructure(u) && hasKey(u, "runs")) return {
|
|
31
|
+
type: "action",
|
|
32
|
+
path: s,
|
|
33
|
+
actions: scanCompositeActionAst(c, l, o)
|
|
34
|
+
};
|
|
35
|
+
} catch {}
|
|
36
|
+
return null;
|
|
37
|
+
}), y = await Promise.all(v);
|
|
38
|
+
for (let e of y) if (e) if (e.type === "workflow") p.workflows.set(e.path, e.actions), p.actions.push(...e.actions);
|
|
39
|
+
else {
|
|
40
|
+
let i = dirname(e.path), a = i === "." || i === "" ? e.path : i;
|
|
41
|
+
p.compositeActions.set(a, e.path), p.actions.push(...e.actions);
|
|
42
|
+
}
|
|
43
|
+
return p;
|
|
44
|
+
}
|
|
45
|
+
function hasKey(e, i) {
|
|
46
|
+
return typeof e == "object" && !!e && i in e;
|
|
47
|
+
}
|
|
48
|
+
export { scanRecursive };
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "1.
|
|
1
|
+
const version = "1.11.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -132,10 +132,21 @@ npx actions-up --dry-run
|
|
|
132
132
|
|
|
133
133
|
### Custom Directory
|
|
134
134
|
|
|
135
|
-
By default, Actions Up scans
|
|
135
|
+
By default, Actions Up scans `.github`.
|
|
136
|
+
|
|
137
|
+
Use `--dir` to choose another directory, and pass it multiple times to scan several directories:
|
|
136
138
|
|
|
137
139
|
```bash
|
|
138
140
|
npx actions-up --dir .gitea
|
|
141
|
+
npx actions-up --dir .github --dir ./other/.github
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Recursive Scanning
|
|
145
|
+
|
|
146
|
+
Use `--recursive` (`-r`) to scan YAML workflow/composite-action files recursively in the selected directories:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npx actions-up --dir ./gh-repo-defaults -r
|
|
139
150
|
```
|
|
140
151
|
|
|
141
152
|
### Branch References
|