actions-up 1.10.1 → 1.12.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 (34) hide show
  1. package/dist/cli/index.js +88 -72
  2. package/dist/cli/merge-scan-results.d.ts +8 -0
  3. package/dist/cli/merge-scan-results.js +18 -0
  4. package/dist/cli/normalize-update-mode.d.ts +8 -0
  5. package/dist/cli/normalize-update-mode.js +6 -0
  6. package/dist/cli/print-mode-warning.d.ts +16 -0
  7. package/dist/cli/print-mode-warning.js +11 -0
  8. package/dist/cli/print-skipped-warning.d.ts +14 -0
  9. package/dist/cli/print-skipped-warning.js +10 -0
  10. package/dist/cli/resolve-scan-directories.d.ts +27 -0
  11. package/dist/cli/resolve-scan-directories.js +24 -0
  12. package/dist/core/api/check-updates.d.ts +4 -1
  13. package/dist/core/api/check-updates.js +79 -89
  14. package/dist/core/api/get-compatible-update.d.ts +27 -0
  15. package/dist/core/api/get-compatible-update.js +40 -0
  16. package/dist/core/ast/scanners/scan-composite-action-ast.js +2 -2
  17. package/dist/core/ast/scanners/scan-workflow-ast.js +7 -7
  18. package/dist/core/fs/find-yaml-files-recursive.d.ts +7 -0
  19. package/dist/core/fs/find-yaml-files-recursive.js +20 -0
  20. package/dist/core/index.d.ts +1 -0
  21. package/dist/core/index.js +2 -1
  22. package/dist/core/scan-action-file.js +1 -1
  23. package/dist/core/scan-github-actions.js +11 -12
  24. package/dist/core/scan-recursive.d.ts +10 -0
  25. package/dist/core/scan-recursive.js +46 -0
  26. package/dist/core/versions/find-compatible-tag.d.ts +16 -0
  27. package/dist/core/versions/find-compatible-tag.js +27 -0
  28. package/dist/core/versions/is-semver-like.d.ts +9 -0
  29. package/dist/core/versions/is-semver-like.js +4 -0
  30. package/dist/core/versions/normalize-version.d.ts +14 -0
  31. package/dist/core/versions/normalize-version.js +9 -0
  32. package/dist/package.js +1 -1
  33. package/package.json +1 -1
  34. package/readme.md +19 -1
package/dist/cli/index.js CHANGED
@@ -1,10 +1,18 @@
1
1
  import { readInlineVersionComment } from "../core/versions/read-inline-version-comment.js";
2
2
  import { isSha } from "../core/versions/is-sha.js";
3
3
  import { promptUpdateSelection } from "../core/interactive/prompt-update-selection.js";
4
+ import { getCompatibleUpdate } from "../core/api/get-compatible-update.js";
5
+ import { createGitHubClient } from "../core/api/create-github-client.js";
6
+ import { resolveScanDirectories } from "./resolve-scan-directories.js";
4
7
  import { getUpdateLevel } from "../core/versions/get-update-level.js";
5
8
  import { applyUpdates } from "../core/ast/update/apply-updates.js";
9
+ import { printSkippedWarning } from "./print-skipped-warning.js";
10
+ import { normalizeUpdateMode } from "./normalize-update-mode.js";
6
11
  import { shouldIgnore } from "../core/ignore/should-ignore.js";
7
12
  import { checkUpdates } from "../core/api/check-updates.js";
13
+ import { mergeScanResults } from "./merge-scan-results.js";
14
+ import { printModeWarning } from "./print-mode-warning.js";
15
+ import { scanRecursive } from "../core/scan-recursive.js";
8
16
  import { scanGitHubActions } from "../core/scan-github-actions.js";
9
17
  import "../core/index.js";
10
18
  import { version } from "../package.js";
@@ -13,106 +21,114 @@ import "node:worker_threads";
13
21
  import pc from "picocolors";
14
22
  import cac from "cac";
15
23
  function run() {
16
- let h = cac("actions-up");
17
- h.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("--yes, -y", "Skip all confirmations").command("", "Update GitHub Actions").action(async (p) => {
24
+ let b = cac("actions-up");
25
+ b.help().version(version).option("--dir <directory>", "Directory to scan (repeatable). Default: .github, or . with --recursive").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 (v) => {
18
26
  console.info(pc.cyan("\n🚀 Actions Up!\n"));
19
- let m = createSpinner("Scanning GitHub Actions...").start();
27
+ let y = createSpinner("Scanning GitHub Actions...").start(), b = resolveScanDirectories({
28
+ recursive: v.recursive,
29
+ cwd: process.cwd(),
30
+ dir: v.dir
31
+ });
20
32
  try {
21
- let h = await scanGitHubActions(process.cwd(), p.dir), g = h.actions.length, _ = h.workflows.size, v = h.compositeActions.size;
22
- if (m.success(`Found ${pc.yellow(g)} actions in ${pc.yellow(_)} workflows and ${pc.yellow(v)} composite actions`), g === 0) {
33
+ let m = mergeScanResults(v.recursive ? await Promise.all(b.map(({ root: t, dir: u }) => scanRecursive(t, u))) : await Promise.all(b.map(({ root: t, dir: u }) => scanGitHubActions(t, u)))), x = m.actions.length, S = m.workflows.size, C = m.compositeActions.size;
34
+ if (y.success(`Found ${pc.yellow(x)} actions in ${pc.yellow(S)} workflows and ${pc.yellow(C)} composite actions`), x === 0) {
23
35
  console.info(pc.green("\n✨ No GitHub Actions found in this repository"));
24
36
  return;
25
37
  }
26
- let y = h.actions, b = [];
27
- Array.isArray(p.exclude) ? b.push(...p.exclude) : typeof p.exclude == "string" && b.push(p.exclude);
28
- let x = b.flatMap((e) => e.split(",")).map((e) => e.trim()).filter(Boolean);
29
- if (x.length > 0) {
30
- let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"), s = e(x);
31
- s.length > 0 && (y = y.filter((e) => {
32
- let { name: c } = e;
33
- for (let e of s) if (e.test(c)) return !1;
38
+ let w = m.actions, T = [];
39
+ Array.isArray(v.exclude) ? T.push(...v.exclude) : typeof v.exclude == "string" && T.push(v.exclude);
40
+ let E = T.flatMap((t) => t.split(",")).map((t) => t.trim()).filter(Boolean);
41
+ if (E.length > 0) {
42
+ let { parseExcludePatterns: t } = await import("../core/filters/parse-exclude-patterns.js"), u = t(E);
43
+ u.length > 0 && (w = w.filter((t) => {
44
+ let { name: d } = t;
45
+ for (let t of u) if (t.test(d)) return !1;
34
46
  return !0;
35
47
  }));
36
48
  }
37
- if (m = createSpinner("Checking for updates...").start(), y.length === 0) {
38
- m.success("No actions to check after excludes"), console.info(pc.green("\n✨ Nothing to check after excludes\n"));
49
+ if (y = createSpinner("Checking for updates...").start(), w.length === 0) {
50
+ y.success("No actions to check after excludes"), console.info(pc.green("\n✨ Nothing to check after excludes\n"));
39
51
  return;
40
52
  }
41
- let S = p.includeBranches ?? !1, C = await checkUpdates(y, process.env.GITHUB_TOKEN, { includeBranches: S }), w = [];
42
- await Promise.all(C.map(async (e) => {
43
- await shouldIgnore(e.action.file, e.action.line) || w.push(e);
53
+ let D = process.env.GITHUB_TOKEN, O = createGitHubClient(D), k = v.includeBranches ?? !1, A = await checkUpdates(w, D, {
54
+ client: O,
55
+ includeBranches: k
56
+ }), j = [];
57
+ await Promise.all(A.map(async (t) => {
58
+ await shouldIgnore(t.action.file, t.action.line) || j.push(t);
44
59
  }));
45
- let T = w.filter((e) => e.status === "skipped"), E = w.filter((e) => e.hasUpdate), D = p.minAge * 24 * 60 * 60 * 1e3, O = Date.now();
46
- E = E.filter((e) => e.publishedAt ? O - e.publishedAt.getTime() >= D : !0);
47
- let k = normalizeUpdateMode(p.mode), A = [];
48
- if (k !== "major") {
49
- let c = /* @__PURE__ */ new Map(), u = await Promise.all(E.map(async (u) => {
50
- let d = u.currentVersion;
51
- if (isSha(u.currentVersion)) {
52
- let s = await readInlineVersionComment(u.action.file, u.action.line, c);
53
- s && (d = s);
60
+ let M = j.filter((t) => t.status === "skipped"), N = j.filter((t) => t.hasUpdate), P = v.minAge * 24 * 60 * 60 * 1e3, F = Date.now();
61
+ N = N.filter((t) => t.publishedAt ? F - t.publishedAt.getTime() >= P : !0);
62
+ let I = normalizeUpdateMode(v.mode), L = [];
63
+ if (I !== "major") {
64
+ let d = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map(), h = await Promise.all(N.map(async (d) => {
65
+ let f = d.currentVersion;
66
+ if (isSha(d.currentVersion)) {
67
+ let u = await readInlineVersionComment(d.action.file, d.action.line, m);
68
+ u && (f = u);
54
69
  }
55
- let f = getUpdateLevel(d, u.latestVersion);
70
+ let p = getUpdateLevel(f, d.latestVersion);
56
71
  return {
57
- allowed: k === "minor" ? f === "minor" || f === "patch" || f === "none" : f === "patch" || f === "none",
58
- update: u
72
+ effectiveCurrentVersion: f,
73
+ allowed: I === "minor" ? p === "minor" || p === "patch" || p === "none" : p === "patch" || p === "none",
74
+ update: d
59
75
  };
60
- })), d = [];
61
- for (let e of u) e.allowed ? d.push(e.update) : A.push(e.update);
62
- E = d;
76
+ })), g = [], _ = await Promise.all(h.map(async (t) => {
77
+ if (t.allowed) return { update: t.update };
78
+ let u = await getCompatibleUpdate(O, {
79
+ currentVersion: t.effectiveCurrentVersion,
80
+ actionName: t.update.action.name,
81
+ tagsCache: d,
82
+ shaCache: p,
83
+ mode: I
84
+ });
85
+ return u ? { update: {
86
+ ...t.update,
87
+ latestVersion: u.version,
88
+ latestSha: u.sha,
89
+ isBreaking: !1,
90
+ hasUpdate: !0
91
+ } } : { blocked: t.update };
92
+ }));
93
+ for (let t of _) {
94
+ if (t.update) {
95
+ g.push(t.update);
96
+ continue;
97
+ }
98
+ L.push(t.blocked);
99
+ }
100
+ N = g;
63
101
  }
64
- let j = E.filter((e) => e.isBreaking);
65
- if (E.length === 0) {
66
- m.success("All actions are up to date!"), T.length > 0 && printSkippedWarning(T, S), A.length > 0 && printModeWarning(A, k), console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
102
+ let R = N.filter((t) => t.isBreaking);
103
+ if (N.length === 0) {
104
+ y.success("All actions are up to date!"), M.length > 0 && printSkippedWarning(M, k), L.length > 0 && printModeWarning(L, I), console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
67
105
  return;
68
106
  }
69
- if (m.success(`Found ${pc.yellow(E.length)} updates available${j.length > 0 ? ` (${pc.redBright(j.length)} breaking)` : ""}`), T.length > 0 && printSkippedWarning(T, S), A.length > 0 && printModeWarning(A, k), p.dryRun) {
107
+ if (y.success(`Found ${pc.yellow(N.length)} updates available${R.length > 0 ? ` (${pc.redBright(R.length)} breaking)` : ""}`), M.length > 0 && printSkippedWarning(M, k), L.length > 0 && printModeWarning(L, I), v.dryRun) {
70
108
  console.info(pc.yellow("\n📋 Dry Run - No changes will be made\n"));
71
- for (let e of E) 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`);
72
- console.info(pc.gray(`\n${E.length} actions would be updated\n`));
109
+ for (let t of N) console.info(`${pc.cyan(t.action.file ?? "unknown")}:\n${t.action.name}: ${pc.redBright(t.currentVersion)} → ${pc.green(t.latestVersion)} ${t.latestSha ? pc.gray(`(${t.latestSha.slice(0, 7)})`) : ""}\n`);
110
+ console.info(pc.gray(`\n${N.length} actions would be updated\n`));
73
111
  return;
74
112
  }
75
- if (p.yes) {
76
- let e = E.filter((e) => e.latestSha);
77
- if (e.length === 0) {
113
+ if (v.yes) {
114
+ let t = N.filter((t) => t.latestSha);
115
+ if (t.length === 0) {
78
116
  console.info(pc.yellow("\n⚠️ No actions with SHA available for update\n"));
79
117
  return;
80
118
  }
81
- console.info(pc.yellow(`\n🔄 Updating ${e.length} actions...\n`)), await applyUpdates(e), console.info(pc.green("\n✓ Updates applied successfully!"));
119
+ console.info(pc.yellow(`\n🔄 Updating ${t.length} actions...\n`)), await applyUpdates(t), console.info(pc.green("\n✓ Updates applied successfully!"));
82
120
  } else {
83
- (T.length > 0 || A.length > 0) && console.info("");
84
- let e = await promptUpdateSelection(E, { showAge: p.minAge > 0 });
85
- if (!e || e.length === 0) {
121
+ (M.length > 0 || L.length > 0) && console.info("");
122
+ let t = await promptUpdateSelection(N, { showAge: v.minAge > 0 });
123
+ if (!t || t.length === 0) {
86
124
  console.info(pc.gray("\nNo updates applied"));
87
125
  return;
88
126
  }
89
- console.info(pc.yellow(`\n🔄 Updating ${e.length} selected actions...\n`)), await applyUpdates(e), console.info(pc.green("\n✓ Updates applied successfully!"));
127
+ console.info(pc.yellow(`\n🔄 Updating ${t.length} selected actions...\n`)), await applyUpdates(t), console.info(pc.green("\n✓ Updates applied successfully!"));
90
128
  }
91
- } catch (e) {
92
- m.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);
129
+ } catch (t) {
130
+ y.error("Failed"), t instanceof Error && t.name === "GitHubRateLimitError" ? (console.error(pc.yellow("\n⚠️ Rate Limit Exceeded\n")), console.error(t.message), console.error(pc.gray("\nExample: GITHUB_TOKEN=ghp_xxxx actions-up\n"))) : console.error(pc.redBright("\nError:"), t instanceof Error ? t.message : String(t)), process.exit(1);
93
131
  }
94
- }), h.parse();
95
- }
96
- function printModeWarning(e, s) {
97
- if (e.length === 0) return;
98
- let c = new Intl.PluralRules("en-US", { type: "cardinal" }).select(e.length) === "one" ? "action" : "actions", l = s === "minor" ? "major" : "major/minor";
99
- console.info(pc.yellow(`\n⚠️ Skipped ${e.length} ${c} due to ${l} updates`));
100
- for (let s of e) {
101
- let e = s.action.uses ?? `${s.action.name}@${s.currentVersion ?? "unknown"}`;
102
- console.info(pc.gray(` • ${e}`));
103
- }
104
- }
105
- function printSkippedWarning(e, s) {
106
- let c = new Intl.PluralRules("en-US", { type: "cardinal" }).select(e.length) === "one" ? "action" : "actions", l = s ? "" : " (use --include-branches to check them)";
107
- console.info(pc.yellow(`\n⚠️ Skipped ${e.length} ${c} pinned to branches${l}`));
108
- for (let s of e) {
109
- let e = s.action.uses ?? `${s.action.name}@${s.currentVersion ?? "unknown"}`;
110
- console.info(pc.gray(` • ${e}`));
111
- }
112
- }
113
- function normalizeUpdateMode(e) {
114
- let s = (e ?? "major").toLowerCase();
115
- if (s === "major" || s === "minor" || s === "patch") return s;
116
- throw Error(`Invalid mode "${e}". Expected "major", "minor", or "patch".`);
132
+ }), b.parse();
117
133
  }
118
134
  export { run };
@@ -0,0 +1,8 @@
1
+ import { ScanResult } from '../types/scan-result';
2
+ /**
3
+ * Merge multiple scan results into one.
4
+ *
5
+ * @param results - Array of scan results.
6
+ * @returns Merged scan result.
7
+ */
8
+ export declare function mergeScanResults(results: ScanResult[]): ScanResult;
@@ -0,0 +1,18 @@
1
+ function mergeScanResults(e) {
2
+ let t = {
3
+ compositeActions: /* @__PURE__ */ new Map(),
4
+ workflows: /* @__PURE__ */ new Map(),
5
+ actions: []
6
+ };
7
+ for (let [n, r] of e.entries()) {
8
+ for (let [e, i] of r.workflows) t.workflows.set(`${n}:${e}`, i);
9
+ for (let [, e] of r.compositeActions) t.compositeActions.set(`${n}:${e}`, e);
10
+ t.actions.push(...r.actions);
11
+ }
12
+ let n = /* @__PURE__ */ new Set();
13
+ return t.actions = t.actions.filter((e) => {
14
+ let t = `${e.file}:${e.line}:${e.name}:${e.version}`;
15
+ return n.has(t) ? !1 : (n.add(t), !0);
16
+ }), t;
17
+ }
18
+ export { mergeScanResults };
@@ -0,0 +1,8 @@
1
+ import { UpdateMode } from '../types/update-mode';
2
+ /**
3
+ * Normalizes the update mode option.
4
+ *
5
+ * @param mode - Raw mode option.
6
+ * @returns Normalized update mode.
7
+ */
8
+ export declare function normalizeUpdateMode(mode: undefined | string): UpdateMode;
@@ -0,0 +1,6 @@
1
+ function normalizeUpdateMode(e) {
2
+ let t = (e ?? "major").toLowerCase();
3
+ if (t === "major" || t === "minor" || t === "patch") return t;
4
+ throw Error(`Invalid mode "${e}". Expected "major", "minor", or "patch".`);
5
+ }
6
+ export { normalizeUpdateMode };
@@ -0,0 +1,16 @@
1
+ import { UpdateMode } from '../types/update-mode';
2
+ /**
3
+ * Prints a warning message for actions that were skipped due to update mode
4
+ * restrictions.
5
+ *
6
+ * @param blocked - Array of blocked actions with their current versions.
7
+ * @param mode - The current update mode (patch/minor/major).
8
+ */
9
+ export declare function printModeWarning(blocked: {
10
+ action: {
11
+ version?: string | null;
12
+ uses?: string;
13
+ name: string;
14
+ };
15
+ currentVersion: string | null;
16
+ }[], mode: UpdateMode): void;
@@ -0,0 +1,11 @@
1
+ import pc from "picocolors";
2
+ function printModeWarning(t, n) {
3
+ if (t.length === 0) return;
4
+ let r = new Intl.PluralRules("en-US", { type: "cardinal" }).select(t.length) === "one" ? "action" : "actions", i = n === "minor" ? "major" : "major/minor";
5
+ console.info(pc.yellow(`\n⚠️ Skipped ${t.length} ${r} due to ${i} updates`));
6
+ for (let n of t) {
7
+ let t = n.action.uses ?? `${n.action.name}@${n.currentVersion ?? "unknown"}`;
8
+ console.info(pc.gray(` • ${t}`));
9
+ }
10
+ }
11
+ export { printModeWarning };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Prints a warning message for actions that were skipped during scanning.
3
+ *
4
+ * @param skipped - Array of skipped actions with their current versions.
5
+ * @param includeBranches - Whether branch-pinned actions are being checked.
6
+ */
7
+ export declare function printSkippedWarning(skipped: {
8
+ action: {
9
+ version?: string | null;
10
+ uses?: string;
11
+ name: string;
12
+ };
13
+ currentVersion: string | null;
14
+ }[], includeBranches: boolean): void;
@@ -0,0 +1,10 @@
1
+ import pc from "picocolors";
2
+ function printSkippedWarning(t, n) {
3
+ let r = new Intl.PluralRules("en-US", { type: "cardinal" }).select(t.length) === "one" ? "action" : "actions", i = n ? "" : " (use --include-branches to check them)";
4
+ console.info(pc.yellow(`\n⚠️ Skipped ${t.length} ${r} pinned to branches${i}`));
5
+ for (let n of t) {
6
+ let t = n.action.uses ?? `${n.action.name}@${n.currentVersion ?? "unknown"}`;
7
+ console.info(pc.gray(` • ${t}`));
8
+ }
9
+ }
10
+ export { printSkippedWarning };
@@ -0,0 +1,27 @@
1
+ /** Options for resolving scan directories from CLI flags. */
2
+ interface ResolveScanDirectoriesOptions {
3
+ dir?: string[] | string;
4
+ recursive?: boolean;
5
+ cwd: string;
6
+ }
7
+ /** Resolved directory with root and relative directory. */
8
+ interface ResolvedDirectory {
9
+ root: string;
10
+ dir: string;
11
+ }
12
+ /**
13
+ * Resolve directories to scan from CLI options.
14
+ *
15
+ * Defaults:
16
+ *
17
+ * - Non-recursive mode: `.github`
18
+ * - Recursive mode without --dir: `.`.
19
+ *
20
+ * @param options - CLI options used to compute scan directories.
21
+ * @param options.cwd - Current working directory.
22
+ * @param options.dir - Optional directory flag value(s).
23
+ * @param options.recursive - Whether recursive mode is enabled.
24
+ * @returns Unique resolved directories.
25
+ */
26
+ export declare function resolveScanDirectories(options: ResolveScanDirectoriesOptions): ResolvedDirectory[];
27
+ export {};
@@ -0,0 +1,24 @@
1
+ import { GITHUB_DIRECTORY } from "../core/constants.js";
2
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
+ function resolveScanDirectories(o) {
4
+ let { recursive: s, cwd: c, dir: l } = o, u = [];
5
+ Array.isArray(l) ? u.push(...l) : typeof l == "string" ? u.push(l) : s ? u.push(".") : u.push(GITHUB_DIRECTORY);
6
+ let d = /* @__PURE__ */ new Set(), f = [];
7
+ for (let e of u) {
8
+ let o = resolve(c, e), l = relative(c, o), u = l.startsWith("..") || isAbsolute(l) || resolve(c, l) !== o, p;
9
+ p = s ? {
10
+ root: o,
11
+ dir: "."
12
+ } : u ? {
13
+ dir: basename(o),
14
+ root: dirname(o)
15
+ } : {
16
+ dir: l || ".github",
17
+ root: c
18
+ };
19
+ let m = `${p.root}\0${p.dir}`;
20
+ d.has(m) || (d.add(m), f.push(p));
21
+ }
22
+ return f;
23
+ }
24
+ export { resolveScanDirectories };
@@ -1,3 +1,4 @@
1
+ import { GitHubClient } from '../../types/github-client';
1
2
  import { GitHubAction } from '../../types/github-action';
2
3
  import { ActionUpdate } from '../../types/action-update';
3
4
  /**
@@ -5,9 +6,11 @@ import { ActionUpdate } from '../../types/action-update';
5
6
  *
6
7
  * @param actions - Array of GitHub Actions to check.
7
8
  * @param token - Optional GitHub token for authentication.
8
- * @param options - Additional options (e.g., include branch refs).
9
+ * @param options - Additional options (e.g., include branch refs, shared
10
+ * client).
9
11
  * @returns Array of update information.
10
12
  */
11
13
  export declare function checkUpdates(actions: GitHubAction[], token?: string, options?: {
12
14
  includeBranches?: boolean;
15
+ client?: GitHubClient;
13
16
  }): Promise<ActionUpdate[]>;
@@ -1,7 +1,9 @@
1
+ import { normalizeVersion } from "../versions/normalize-version.js";
2
+ import { isSemverLike } from "../versions/is-semver-like.js";
1
3
  import { createGitHubClient } from "./create-github-client.js";
2
4
  import semver from "semver";
3
- async function checkUpdates(n, i, c) {
4
- let l = createGitHubClient(i), u = c?.includeBranches ?? !1, d = n.filter((e) => e.type === "external" || e.type === "reusable-workflow");
5
+ async function checkUpdates(i, o, c) {
6
+ let l = c?.client ?? createGitHubClient(o), u = c?.includeBranches ?? !1, d = i.filter((e) => e.type === "external" || e.type === "reusable-workflow");
5
7
  if (d.length === 0) return [];
6
8
  let f = /* @__PURE__ */ new Map();
7
9
  for (let e of d) {
@@ -11,135 +13,135 @@ async function checkUpdates(n, i, c) {
11
13
  let p = {
12
14
  rateLimitError: null,
13
15
  rateLimitHit: !1
14
- }, m = await [...f.keys()].reduce((e, n) => e.then(async (e) => {
15
- if (p.rateLimitHit) return [...e, {
16
+ }, m = await [...f.keys()].reduce((n, i) => n.then(async (n) => {
17
+ if (p.rateLimitHit) return [...n, {
16
18
  publishedAt: null,
17
19
  version: null,
18
- actionName: n,
20
+ actionName: i,
19
21
  sha: null
20
22
  }];
21
- let r = n.split("/");
22
- if (r.length < 2) return [...e, {
23
+ let a = i.split("/");
24
+ if (a.length < 2) return [...n, {
23
25
  publishedAt: null,
24
26
  version: null,
25
- actionName: n,
27
+ actionName: i,
26
28
  sha: null
27
29
  }];
28
- let [i, c] = r;
29
- if (!i || !c) return [...e, {
30
+ let [o, c] = a;
31
+ if (!o || !c) return [...n, {
30
32
  publishedAt: null,
31
33
  version: null,
32
- actionName: n,
34
+ actionName: i,
33
35
  sha: null
34
36
  }];
35
37
  try {
36
- let r = f.get(n)[0]?.version;
37
- if (r && !isSha(r) && !isSemverLike(r) && await l.getRefType(i, c, r) === "branch" && !u) return [...e, {
38
+ let a = f.get(i)[0]?.version;
39
+ if (a && !isSha(a) && !isSemverLike(a) && await l.getRefType(o, c, a) === "branch" && !u) return [...n, {
38
40
  skipReason: "branch",
39
41
  status: "skipped",
40
42
  publishedAt: null,
41
43
  version: null,
42
- actionName: n,
44
+ actionName: i,
43
45
  sha: null
44
46
  }];
45
- let d = await l.getLatestRelease(i, c);
47
+ let d = await l.getLatestRelease(o, c);
46
48
  if (!d) {
47
- let e = await l.getAllReleases(i, c, 1);
49
+ let e = await l.getAllReleases(o, c, 1);
48
50
  d = e.find((e) => !e.isPrerelease) ?? e[0] ?? null;
49
51
  }
50
52
  if (d) {
51
- let { publishedAt: r, version: o, sha: u } = d, f = !1;
53
+ let { publishedAt: a, version: s, sha: u } = d, f = !1;
52
54
  {
53
- let e = normalizeVersion(o), n = !!(o && o.trim() !== ""), r = n && /^v?\d+$/u.test(o.trim()), i = semver.valid(e);
54
- f = !n || r || !i || !isSemverLike(o);
55
+ let n = normalizeVersion(s), i = !!(s && s.trim() !== ""), a = i && /^v?\d+$/u.test(s.trim()), o = semver.valid(n);
56
+ f = !i || a || !o || !isSemverLike(s);
55
57
  }
56
58
  if (f) {
57
- let r = await l.getAllTags(i, c, 30);
58
- if (r.length > 0) {
59
- let u = r.filter((e) => isSemverLike(e.tag)).map((e) => ({
60
- v: semver.valid(normalizeVersion(e.tag)),
61
- raw: e
59
+ let a = await l.getAllTags(o, c, 30);
60
+ if (a.length > 0) {
61
+ let u = a.filter((e) => isSemverLike(e.tag)).map((t) => ({
62
+ v: semver.valid(normalizeVersion(t.tag)),
63
+ raw: t
62
64
  }));
63
65
  if (u.length > 0) {
64
- u.sort((e, n) => {
65
- let r = semver.rcompare(e.v, n.v);
66
- if (r !== 0) return r;
66
+ u.sort((e, t) => {
67
+ let n = semver.rcompare(e.v, t.v);
68
+ if (n !== 0) return n;
67
69
  let i = /\d+\.\d+/u.test(e.raw.tag) ? 1 : 0;
68
- return (/\d+\.\d+/u.test(n.raw.tag) ? 1 : 0) - i;
70
+ return (/\d+\.\d+/u.test(t.raw.tag) ? 1 : 0) - i;
69
71
  });
70
- let r = u[0].raw, s = semver.valid(normalizeVersion(o) ?? void 0);
71
- if (!s || semver.gt(u[0].v, s) || semver.eq(u[0].v, s) && /\d+\.\d+/u.test(r.tag)) {
72
- let t = r.tag, a = r.sha?.length ? r.sha : null;
73
- if (!a && t) try {
74
- a = await l.getTagSha(i, c, t);
72
+ let t = u[0].raw, a = semver.valid(normalizeVersion(s) ?? void 0);
73
+ if (!a || semver.gt(u[0].v, a) || semver.eq(u[0].v, a) && /\d+\.\d+/u.test(t.tag)) {
74
+ let e = t.tag, r = t.sha?.length ? t.sha : null;
75
+ if (!r && e) try {
76
+ r = await l.getTagSha(o, c, e);
75
77
  } catch {}
76
- return [...e, {
77
- version: t,
78
+ return [...n, {
79
+ version: e,
78
80
  publishedAt: null,
79
- sha: a,
80
- actionName: n
81
+ sha: r,
82
+ actionName: i
81
83
  }];
82
84
  }
83
85
  }
84
86
  }
85
87
  }
86
- if (!u && o) try {
87
- u = await l.getTagSha(i, c, o);
88
+ if (!u && s) try {
89
+ u = await l.getTagSha(o, c, s);
88
90
  } catch {}
89
- return [...e, {
91
+ return [...n, {
90
92
  status: "ok",
91
- publishedAt: r,
92
- actionName: n,
93
- version: o,
93
+ publishedAt: a,
94
+ actionName: i,
95
+ version: s,
94
96
  sha: u
95
97
  }];
96
98
  }
97
- let p = await l.getAllTags(i, c, 30);
99
+ let p = await l.getAllTags(o, c, 30);
98
100
  if (p.length > 0) {
99
- let r = p.filter((e) => isSemverLike(e.tag)).map((e) => ({
100
- v: semver.valid(normalizeVersion(e.tag)),
101
- raw: e
102
- })), o;
103
- r.length > 0 ? (r.sort((e, n) => {
104
- let r = semver.rcompare(e.v, n.v);
105
- if (r !== 0) return r;
101
+ let a = p.filter((e) => isSemverLike(e.tag)).map((t) => ({
102
+ v: semver.valid(normalizeVersion(t.tag)),
103
+ raw: t
104
+ })), s;
105
+ a.length > 0 ? (a.sort((e, t) => {
106
+ let n = semver.rcompare(e.v, t.v);
107
+ if (n !== 0) return n;
106
108
  let i = /\d+\.\d+/u.test(e.raw.tag) ? 1 : 0;
107
- return (/\d+\.\d+/u.test(n.raw.tag) ? 1 : 0) - i;
108
- }), o = r[0].raw) : o = p[0];
109
- let u = o.tag, d = o.sha?.length ? o.sha : null;
109
+ return (/\d+\.\d+/u.test(t.raw.tag) ? 1 : 0) - i;
110
+ }), s = a[0].raw) : s = p[0];
111
+ let u = s.tag, d = s.sha?.length ? s.sha : null;
110
112
  if (!d && u) try {
111
- d = await l.getTagSha(i, c, u);
113
+ d = await l.getTagSha(o, c, u);
112
114
  } catch {}
113
- return [...e, {
115
+ return [...n, {
114
116
  status: "ok",
115
117
  publishedAt: null,
116
- actionName: n,
118
+ actionName: i,
117
119
  version: u,
118
120
  sha: d
119
121
  }];
120
122
  }
121
- return [...e, {
123
+ return [...n, {
122
124
  publishedAt: null,
123
125
  version: null,
124
- actionName: n,
126
+ actionName: i,
125
127
  sha: null
126
128
  }];
127
- } catch (t) {
128
- return t instanceof Error && t.name === "GitHubRateLimitError" ? (p.rateLimitHit = !0, p.rateLimitError = t, [...e, {
129
+ } catch (e) {
130
+ return e instanceof Error && e.name === "GitHubRateLimitError" ? (p.rateLimitHit = !0, p.rateLimitError = e, [...n, {
129
131
  publishedAt: null,
130
132
  version: null,
131
- actionName: n,
133
+ actionName: i,
132
134
  sha: null
133
- }]) : (console.warn(`Failed to check ${n}:`, t), [...e, {
135
+ }]) : (console.warn(`Failed to check ${i}:`, e), [...n, {
134
136
  publishedAt: null,
135
137
  version: null,
136
- actionName: n,
138
+ actionName: i,
137
139
  sha: null
138
140
  }]);
139
141
  }
140
142
  }), Promise.resolve([]));
141
143
  if (p.rateLimitError) {
142
- let e = !!(i ?? process.env.GITHUB_TOKEN), t = `${p.rateLimitError.message || "GitHub API rate limit exceeded."}\n${e ? "Wait for reset or reduce request rate." : "Please set GITHUB_TOKEN environment variable to increase the limit.\nSee: https://github.com/azat-io/actions-up?tab=readme-ov-file#using-github-token-for-higher-rate-limits"}`, n = Error(t);
144
+ let e = !!(o ?? process.env.GITHUB_TOKEN), t = `${p.rateLimitError.message || "GitHub API rate limit exceeded."}\n${e ? "Wait for reset or reduce request rate." : "Please set GITHUB_TOKEN environment variable to increase the limit.\nSee: https://github.com/azat-io/actions-up?tab=readme-ov-file#using-github-token-for-higher-rate-limits"}`, n = Error(t);
143
145
  throw n.name = "GitHubRateLimitError", n;
144
146
  }
145
147
  let h = /* @__PURE__ */ new Map();
@@ -169,39 +171,39 @@ async function checkUpdates(n, i, c) {
169
171
  }
170
172
  return g;
171
173
  }
172
- function createUpdate(e, n, r = {}) {
173
- let { version: s, sha: c, publishedAt: l } = n, u = e.version ?? "unknown", d = normalizeVersion(u), f = s ? normalizeVersion(s) : null, p = r.status ?? "ok", m = r.skipReason, h = !1, g = !1;
174
+ function createUpdate(t, n, i = {}) {
175
+ let { version: a, sha: c, publishedAt: l } = n, u = t.version ?? "unknown", d = normalizeVersion(u), f = a ? normalizeVersion(a) : null, p = i.status ?? "ok", m = i.skipReason, h = !1, g = !1;
174
176
  if (p === "skipped") return {
175
177
  currentVersion: u,
176
178
  isBreaking: !1,
177
179
  hasUpdate: !1,
178
- latestVersion: s,
180
+ latestVersion: a,
179
181
  publishedAt: l,
180
182
  skipReason: m,
181
183
  latestSha: c,
182
- action: e,
184
+ action: t,
183
185
  status: p
184
186
  };
185
187
  if (d && isSha(d)) c ? h = !compareSha(d, c) : f && (h = !0);
186
188
  else if (d && f) {
187
- let n = semver.valid(d), r = semver.valid(f);
188
- if (n && r) {
189
- if (h = semver.lt(n, r), h) {
190
- let e = semver.major(n);
191
- g = semver.major(r) > e;
189
+ let e = semver.valid(d), n = semver.valid(f);
190
+ if (e && n) {
191
+ if (h = semver.lt(e, n), h) {
192
+ let t = semver.major(e);
193
+ g = semver.major(n) > t;
192
194
  }
193
- !h && semver.eq(n, r) && !isSha(e.version) && c && (h = !0, g = !1);
195
+ !h && semver.eq(e, n) && !isSha(t.version) && c && (h = !0, g = !1);
194
196
  } else d !== f && (h = !0);
195
197
  }
196
198
  return {
197
199
  currentVersion: u,
198
- latestVersion: s,
200
+ latestVersion: a,
199
201
  publishedAt: l,
200
202
  isBreaking: g,
201
203
  skipReason: m,
202
204
  latestSha: c,
203
205
  hasUpdate: h,
204
- action: e,
206
+ action: t,
205
207
  status: p
206
208
  };
207
209
  }
@@ -209,21 +211,9 @@ function compareSha(e, t) {
209
211
  let n = e.replace(/^v/u, ""), r = t.replace(/^v/u, ""), i = Math.min(n.length, r.length);
210
212
  return i < 7 ? !1 : n.slice(0, Math.max(0, i)).toLowerCase() === r.slice(0, Math.max(0, i)).toLowerCase();
211
213
  }
212
- function normalizeVersion(e) {
213
- if (!e) return null;
214
- let n = e.replace(/^v/u, "");
215
- if (/^[0-9a-f]{7,40}$/iu.test(n)) return e;
216
- let r = semver.coerce(n);
217
- return r ? r.version : e;
218
- }
219
214
  function isSha(e) {
220
215
  if (!e) return !1;
221
216
  let t = e.replace(/^v/u, "");
222
217
  return /^[0-9a-f]{7,40}$/iu.test(t);
223
218
  }
224
- function isSemverLike(e) {
225
- if (!e) return !1;
226
- let t = e.trim();
227
- return /^v?\d+(?:\.\d+){0,2}$/u.test(t);
228
- }
229
219
  export { checkUpdates };
@@ -0,0 +1,27 @@
1
+ import { GitHubClient } from '../../types/github-client';
2
+ import { UpdateMode } from '../../types/update-mode';
3
+ import { TagInfo } from '../../types/tag-info';
4
+ interface GetCompatibleUpdateParameters {
5
+ /** Optional in-memory cache for resolved tag SHAs. */
6
+ shaCache?: Map<string, string | null>;
7
+ /** Update mode that limits which tag can be selected. */
8
+ mode: Exclude<UpdateMode, 'major'>;
9
+ /** Optional in-memory cache for action tags. */
10
+ tagsCache?: Map<string, TagInfo[]>;
11
+ /** Current action version used as compatibility baseline. */
12
+ currentVersion: string | null;
13
+ /** Action name in `owner/repo` format (path suffix is allowed). */
14
+ actionName: string;
15
+ }
16
+ /**
17
+ * Resolve the newest compatible update for an action.
18
+ *
19
+ * @param client - GitHub client instance.
20
+ * @param parameters - Lookup parameters.
21
+ * @returns Compatible target version and SHA, or null when none found.
22
+ */
23
+ export declare function getCompatibleUpdate(client: GitHubClient, parameters: GetCompatibleUpdateParameters): Promise<{
24
+ sha: string | null;
25
+ version: string;
26
+ } | null>;
27
+ export {};
@@ -0,0 +1,40 @@
1
+ import { isSemverLike } from "../versions/is-semver-like.js";
2
+ import { findCompatibleTag } from "../versions/find-compatible-tag.js";
3
+ async function getCompatibleUpdate(n, r) {
4
+ let { currentVersion: i, actionName: a, mode: o } = r;
5
+ if (!i || !isSemverLike(i)) return null;
6
+ let s = a.split("/");
7
+ if (s.length < 2) return null;
8
+ let [c, l] = s;
9
+ if (!c || !l) return null;
10
+ let u = r.tagsCache ?? /* @__PURE__ */ new Map(), d = r.shaCache ?? /* @__PURE__ */ new Map(), f = u.get(a);
11
+ if (!f) {
12
+ try {
13
+ f = await n.getAllTags(c, l, 100);
14
+ } catch {
15
+ return null;
16
+ }
17
+ u.set(a, f);
18
+ }
19
+ let p = findCompatibleTag(f, i, o);
20
+ if (!p) return null;
21
+ let m = p.tag, h = p.sha?.length ? p.sha : null;
22
+ if (!h) {
23
+ let e = `${a}@${m}`;
24
+ if (d.has(e)) return {
25
+ sha: d.get(e) ?? null,
26
+ version: m
27
+ };
28
+ try {
29
+ h = await n.getTagSha(c, l, m);
30
+ } catch {
31
+ h = null;
32
+ }
33
+ d.set(e, h);
34
+ }
35
+ return {
36
+ version: m,
37
+ sha: h
38
+ };
39
+ }
40
+ export { getCompatibleUpdate };
@@ -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 e of f.value.items) {
16
- if (!isPair(e) || !e.value || !isNode(e.value) || !isYAMLMap(e.value)) continue;
17
- let l = isScalar(e.key) ? String(e.key.value) : void 0, f = findMapPair(e.value, "uses");
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 e = parseActionReference(String(f.value.value), d, getLineNumberForKey(u, f.key));
20
- e && (l && (e.job = l), p.push(e));
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(e.value, "steps");
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 { lstat, readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
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 };
@@ -1,3 +1,4 @@
1
1
  export { scanGitHubActions } from './scan-github-actions';
2
2
  export { applyUpdates } from './ast/update/apply-updates';
3
3
  export { checkUpdates } from './api/check-updates';
4
+ export { scanRecursive } from './scan-recursive';
@@ -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 };
@@ -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,7 +1,7 @@
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
5
  import { readFile, readdir, stat } from "node:fs/promises";
6
6
  import { isAbsolute, join, relative, resolve } from "node:path";
7
7
  async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
@@ -15,7 +15,6 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
15
15
  return s !== "" && !s.startsWith("..") && !isAbsolute(s);
16
16
  }
17
17
  let v = join(g, m);
18
- if (!_(g, v)) throw Error("Invalid path: detected path traversal attempt");
19
18
  function y(e) {
20
19
  return e.includes("..") || e.includes("/") || e.includes("\\") ? (console.warn(`Skipping invalid name: ${e}`), !1) : !0;
21
20
  }
@@ -33,11 +32,11 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
33
32
  let e = (await readdir(x)).filter((e) => y(e) ? isYamlFile(e) : !1).map(async (e) => {
34
33
  let o = join(x, e);
35
34
  try {
36
- let l = await scanWorkflowFile(o);
35
+ let c = await scanWorkflowFile(o);
37
36
  return {
38
37
  path: `${m}/${WORKFLOWS_DIRECTORY}/${e}`,
39
38
  success: !0,
40
- actions: l
39
+ actions: c
41
40
  };
42
41
  } catch {
43
42
  return {
@@ -75,12 +74,12 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
75
74
  let s = join(S, o);
76
75
  try {
77
76
  if (!(await stat(s)).isDirectory()) return null;
78
- let c = join(s, "action.yml"), u = [];
77
+ let c = join(s, "action.yml"), l = [];
79
78
  try {
80
- u = await scanActionFile(c);
79
+ l = await scanActionFile(c);
81
80
  } catch {
82
81
  try {
83
- c = join(s, "action.yaml"), u = await scanActionFile(c);
82
+ c = join(s, "action.yaml"), l = await scanActionFile(c);
84
83
  } catch {
85
84
  return null;
86
85
  }
@@ -88,7 +87,7 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
88
87
  return {
89
88
  path: `${m}/${ACTIONS_DIRECTORY}/${o}`,
90
89
  name: o,
91
- actions: u
90
+ actions: l
92
91
  };
93
92
  } catch {
94
93
  return null;
@@ -111,14 +110,14 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
111
110
  }
112
111
  async function c() {
113
112
  if (s.length === 0) return;
114
- let u = s.splice(0), d = await Promise.all(u.map(async (s) => {
113
+ let l = s.splice(0), d = await Promise.all(l.map(async (s) => {
115
114
  try {
116
- let c = join(s, "action.yml"), u = join(s, "action.yaml"), d = c;
115
+ let c = join(s, "action.yml"), l = join(s, "action.yaml"), d = c;
117
116
  try {
118
117
  if (!(await stat(c)).isFile()) throw Error("not a file");
119
118
  } catch {
120
- if (!(await stat(u)).isFile()) throw Error("not a file");
121
- d = u;
119
+ if (!(await stat(l)).isFile()) throw Error("not a file");
120
+ d = l;
122
121
  }
123
122
  let f = await scanActionFile(d);
124
123
  f.length > 0 && h.actions.push(...f);
@@ -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,46 @@
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, relative, resolve } from "node:path";
8
+ async function scanRecursive(u, d) {
9
+ let f = {
10
+ compositeActions: /* @__PURE__ */ new Map(),
11
+ workflows: /* @__PURE__ */ new Map(),
12
+ actions: []
13
+ }, p = resolve(u), m = resolve(p, d), h;
14
+ try {
15
+ h = await findYamlFilesRecursive(m);
16
+ } catch {
17
+ return f;
18
+ }
19
+ let g = h.map(async (o) => {
20
+ let s = relative(p, o);
21
+ try {
22
+ let { document: c, content: l } = await readYamlDocument(o), u = c.toJSON();
23
+ if (isWorkflowStructure(u) && hasKey(u, "jobs")) return {
24
+ type: "workflow",
25
+ path: s,
26
+ actions: scanWorkflowAst(c, l, o)
27
+ };
28
+ if (isCompositeActionStructure(u) && hasKey(u, "runs")) return {
29
+ type: "action",
30
+ path: s,
31
+ actions: scanCompositeActionAst(c, l, o)
32
+ };
33
+ } catch {}
34
+ return null;
35
+ }), _ = await Promise.all(g);
36
+ for (let e of _) if (e) if (e.type === "workflow") f.workflows.set(e.path, e.actions), f.actions.push(...e.actions);
37
+ else {
38
+ let i = dirname(e.path), a = i === "." || i === "" ? e.path : i;
39
+ f.compositeActions.set(a, e.path), f.actions.push(...e.actions);
40
+ }
41
+ return f;
42
+ }
43
+ function hasKey(e, i) {
44
+ return typeof e == "object" && !!e && i in e;
45
+ }
46
+ export { scanRecursive };
@@ -0,0 +1,16 @@
1
+ import { UpdateMode } from '../../types/update-mode';
2
+ import { TagInfo } from '../../types/tag-info';
3
+ /**
4
+ * Pick the newest compatible tag for the provided update mode.
5
+ *
6
+ * Compatibility rules:
7
+ *
8
+ * - `minor`: same major, greater than current.
9
+ * - `patch`: same major and minor, greater than current.
10
+ *
11
+ * @param tags - Available tags from GitHub API.
12
+ * @param currentVersion - Current action version.
13
+ * @param mode - Mode that limits the allowed update level.
14
+ * @returns Best compatible tag or null when no compatible candidate exists.
15
+ */
16
+ export declare function findCompatibleTag(tags: TagInfo[], currentVersion: string | null, mode: Exclude<UpdateMode, 'major'>): TagInfo | null;
@@ -0,0 +1,27 @@
1
+ import { normalizeVersion } from "./normalize-version.js";
2
+ import { isSemverLike } from "./is-semver-like.js";
3
+ import semver from "semver";
4
+ function findCompatibleTag(r, a, o) {
5
+ if (!a || !isSemverLike(a) || r.length === 0) return null;
6
+ let s = semver.valid(normalizeVersion(a));
7
+ if (!s) return null;
8
+ let c = semver.major(s), l = semver.minor(s), u = [];
9
+ for (let i of r) {
10
+ if (!isSemverLike(i.tag)) continue;
11
+ let r = semver.valid(normalizeVersion(i.tag));
12
+ r && semver.gt(r, s) && semver.major(r) === c && (o === "patch" && semver.minor(r) !== l || u.push({
13
+ tag: i,
14
+ parsed: r
15
+ }));
16
+ }
17
+ return u.length === 0 ? null : (u.sort((e, n) => {
18
+ let r = semver.rcompare(e.parsed, n.parsed);
19
+ if (r !== 0) return r;
20
+ let a = getSemverSpecificity(e.tag.tag);
21
+ return getSemverSpecificity(n.tag.tag) - a;
22
+ }), u[0].tag);
23
+ }
24
+ function getSemverSpecificity(e) {
25
+ return e.replace(/^v/u, "").split(".").length;
26
+ }
27
+ export { findCompatibleTag };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Check whether the value follows a semver-like tag pattern.
3
+ *
4
+ * Examples of accepted values: `v1`, `v1.2`, `1.2.3`.
5
+ *
6
+ * @param value - Raw value to validate.
7
+ * @returns True when value looks like a semver-like tag.
8
+ */
9
+ export declare function isSemverLike(value: undefined | string | null): boolean;
@@ -0,0 +1,4 @@
1
+ function isSemverLike(e) {
2
+ return typeof e == "string" && /^v?\d+(?:\.\d+){0,2}$/u.test(e.trim());
3
+ }
4
+ export { isSemverLike };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalize version string.
3
+ *
4
+ * Rules:
5
+ *
6
+ * - Remove `v` prefix before semver coercion.
7
+ * - Preserve SHA-like values as-is.
8
+ * - Return coerced semver when possible.
9
+ * - Return original value when coercion fails.
10
+ *
11
+ * @param version - Version string to normalize.
12
+ * @returns Normalized version string or null if input is empty.
13
+ */
14
+ export declare function normalizeVersion(version: undefined | string | null): string | null;
@@ -0,0 +1,9 @@
1
+ import semver from "semver";
2
+ function normalizeVersion(t) {
3
+ if (!t) return null;
4
+ let n = t.replace(/^v/u, "");
5
+ if (/^[0-9a-f]{7,40}$/iu.test(n)) return t;
6
+ let r = semver.coerce(n);
7
+ return r ? r.version : t;
8
+ }
9
+ export { normalizeVersion };
package/dist/package.js CHANGED
@@ -1,2 +1,2 @@
1
- const version = "1.10.1";
1
+ const version = "1.12.0";
2
2
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actions-up",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
5
5
  "keywords": [
6
6
  "github-actions",
package/readme.md CHANGED
@@ -132,12 +132,26 @@ npx actions-up --dry-run
132
132
 
133
133
  ### Custom Directory
134
134
 
135
- By default, Actions Up scans the `.github` directory. You can specify a different directory (e.g., for Gitea):
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 -r
150
+ npx actions-up --dir ./gh-repo-defaults -r
139
151
  ```
140
152
 
153
+ When `--recursive` is used without `--dir`, Actions Up scans from the current directory (`.`).
154
+
141
155
  ### Branch References
142
156
 
143
157
  By default, actions pinned to branch refs (e.g., `@main`, `@release/v1`) are skipped to avoid changing intentionally floating references. Skipped entries are listed in the output. To include them in update checks, pass `--include-branches`.
@@ -151,6 +165,10 @@ npx actions-up --mode minor
151
165
  npx actions-up --mode patch
152
166
  ```
153
167
 
168
+ In `minor` and `patch` modes, Actions Up tries to find the newest compatible
169
+ tag first (for example, from `@v4` in `minor` mode it will choose the latest
170
+ `v4.x.y`). If no compatible version exists, that action is skipped.
171
+
154
172
  ## GitHub Actions Integration
155
173
 
156
174
  ### Automated PR Checks