actions-up 1.11.0 → 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.
- package/dist/cli/index.js +84 -93
- package/dist/cli/merge-scan-results.d.ts +8 -0
- package/dist/cli/merge-scan-results.js +18 -0
- package/dist/cli/normalize-update-mode.d.ts +8 -0
- package/dist/cli/normalize-update-mode.js +6 -0
- package/dist/cli/print-mode-warning.d.ts +16 -0
- package/dist/cli/print-mode-warning.js +11 -0
- package/dist/cli/print-skipped-warning.d.ts +14 -0
- package/dist/cli/print-skipped-warning.js +10 -0
- package/dist/cli/resolve-scan-directories.d.ts +27 -0
- package/dist/cli/resolve-scan-directories.js +24 -0
- package/dist/core/api/check-updates.d.ts +4 -1
- package/dist/core/api/check-updates.js +79 -89
- package/dist/core/api/get-compatible-update.d.ts +27 -0
- package/dist/core/api/get-compatible-update.js +40 -0
- package/dist/core/fs/find-yaml-files-recursive.js +1 -1
- package/dist/core/interactive/prompt-update-selection.js +9 -9
- package/dist/core/scan-github-actions.js +67 -68
- package/dist/core/scan-recursive.js +12 -14
- package/dist/core/versions/find-compatible-tag.d.ts +16 -0
- package/dist/core/versions/find-compatible-tag.js +27 -0
- package/dist/core/versions/is-semver-like.d.ts +9 -0
- package/dist/core/versions/is-semver-like.js +4 -0
- package/dist/core/versions/normalize-version.d.ts +14 -0
- package/dist/core/versions/normalize-version.js +9 -0
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/readme.md +7 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,143 +1,134 @@
|
|
|
1
1
|
import { readInlineVersionComment } from "../core/versions/read-inline-version-comment.js";
|
|
2
|
-
import { GITHUB_DIRECTORY } from "../core/constants.js";
|
|
3
2
|
import { isSha } from "../core/versions/is-sha.js";
|
|
4
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";
|
|
5
7
|
import { getUpdateLevel } from "../core/versions/get-update-level.js";
|
|
6
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";
|
|
7
11
|
import { shouldIgnore } from "../core/ignore/should-ignore.js";
|
|
8
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";
|
|
9
15
|
import { scanRecursive } from "../core/scan-recursive.js";
|
|
10
16
|
import { scanGitHubActions } from "../core/scan-github-actions.js";
|
|
11
17
|
import "../core/index.js";
|
|
12
18
|
import { version } from "../package.js";
|
|
13
|
-
import { relative, resolve } from "node:path";
|
|
14
19
|
import { createSpinner } from "nanospinner";
|
|
15
20
|
import "node:worker_threads";
|
|
16
21
|
import pc from "picocolors";
|
|
17
22
|
import cac from "cac";
|
|
18
23
|
function run() {
|
|
19
24
|
let b = cac("actions-up");
|
|
20
|
-
b.help().version(version).option("--dir <directory>", "
|
|
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) => {
|
|
21
26
|
console.info(pc.cyan("\n🚀 Actions Up!\n"));
|
|
22
|
-
let y = createSpinner("Scanning GitHub Actions...").start(), b =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}))];
|
|
27
|
+
let y = createSpinner("Scanning GitHub Actions...").start(), b = resolveScanDirectories({
|
|
28
|
+
recursive: v.recursive,
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
dir: v.dir
|
|
31
|
+
});
|
|
28
32
|
try {
|
|
29
|
-
let
|
|
30
|
-
if (y.success(`Found ${pc.yellow(
|
|
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) {
|
|
31
35
|
console.info(pc.green("\n✨ No GitHub Actions found in this repository"));
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
|
-
let
|
|
35
|
-
Array.isArray(
|
|
36
|
-
let
|
|
37
|
-
if (
|
|
38
|
-
let { parseExcludePatterns:
|
|
39
|
-
|
|
40
|
-
let { name:
|
|
41
|
-
for (let
|
|
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;
|
|
42
46
|
return !0;
|
|
43
47
|
}));
|
|
44
48
|
}
|
|
45
|
-
if (y = createSpinner("Checking for updates...").start(),
|
|
49
|
+
if (y = createSpinner("Checking for updates...").start(), w.length === 0) {
|
|
46
50
|
y.success("No actions to check after excludes"), console.info(pc.green("\n✨ Nothing to check after excludes\n"));
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
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);
|
|
52
59
|
}));
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
if (
|
|
57
|
-
let d = /* @__PURE__ */ new Map(), p = await Promise.all(
|
|
58
|
-
let
|
|
59
|
-
if (isSha(
|
|
60
|
-
let
|
|
61
|
-
|
|
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);
|
|
62
69
|
}
|
|
63
|
-
let
|
|
70
|
+
let p = getUpdateLevel(f, d.latestVersion);
|
|
64
71
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
effectiveCurrentVersion: f,
|
|
73
|
+
allowed: I === "minor" ? p === "minor" || p === "patch" || p === "none" : p === "patch" || p === "none",
|
|
74
|
+
update: d
|
|
67
75
|
};
|
|
68
|
-
})),
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
101
|
}
|
|
72
|
-
let
|
|
73
|
-
if (
|
|
74
|
-
y.success("All actions are up to date!"),
|
|
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"));
|
|
75
105
|
return;
|
|
76
106
|
}
|
|
77
|
-
if (y.success(`Found ${pc.yellow(
|
|
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) {
|
|
78
108
|
console.info(pc.yellow("\n📋 Dry Run - No changes will be made\n"));
|
|
79
|
-
for (let
|
|
80
|
-
console.info(pc.gray(`\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`));
|
|
81
111
|
return;
|
|
82
112
|
}
|
|
83
|
-
if (
|
|
84
|
-
let
|
|
85
|
-
if (
|
|
113
|
+
if (v.yes) {
|
|
114
|
+
let t = N.filter((t) => t.latestSha);
|
|
115
|
+
if (t.length === 0) {
|
|
86
116
|
console.info(pc.yellow("\n⚠️ No actions with SHA available for update\n"));
|
|
87
117
|
return;
|
|
88
118
|
}
|
|
89
|
-
console.info(pc.yellow(`\n🔄 Updating ${
|
|
119
|
+
console.info(pc.yellow(`\n🔄 Updating ${t.length} actions...\n`)), await applyUpdates(t), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
90
120
|
} else {
|
|
91
|
-
(
|
|
92
|
-
let
|
|
93
|
-
if (!
|
|
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) {
|
|
94
124
|
console.info(pc.gray("\nNo updates applied"));
|
|
95
125
|
return;
|
|
96
126
|
}
|
|
97
|
-
console.info(pc.yellow(`\n🔄 Updating ${
|
|
127
|
+
console.info(pc.yellow(`\n🔄 Updating ${t.length} selected actions...\n`)), await applyUpdates(t), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
98
128
|
}
|
|
99
|
-
} catch (
|
|
100
|
-
y.error("Failed"),
|
|
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);
|
|
101
131
|
}
|
|
102
132
|
}), b.parse();
|
|
103
133
|
}
|
|
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) {
|
|
122
|
-
if (e.length === 0) return;
|
|
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"}`;
|
|
127
|
-
console.info(pc.gray(` • ${e}`));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
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"}`;
|
|
135
|
-
console.info(pc.gray(` • ${e}`));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function normalizeUpdateMode(e) {
|
|
139
|
-
let d = (e ?? "major").toLowerCase();
|
|
140
|
-
if (d === "major" || d === "minor" || d === "patch") return d;
|
|
141
|
-
throw Error(`Invalid mode "${e}". Expected "major", "minor", or "patch".`);
|
|
142
|
-
}
|
|
143
134
|
export { run };
|
|
@@ -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,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(
|
|
4
|
-
let l = createGitHubClient(
|
|
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((
|
|
15
|
-
if (p.rateLimitHit) return [...
|
|
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:
|
|
20
|
+
actionName: i,
|
|
19
21
|
sha: null
|
|
20
22
|
}];
|
|
21
|
-
let
|
|
22
|
-
if (
|
|
23
|
+
let a = i.split("/");
|
|
24
|
+
if (a.length < 2) return [...n, {
|
|
23
25
|
publishedAt: null,
|
|
24
26
|
version: null,
|
|
25
|
-
actionName:
|
|
27
|
+
actionName: i,
|
|
26
28
|
sha: null
|
|
27
29
|
}];
|
|
28
|
-
let [
|
|
29
|
-
if (!
|
|
30
|
+
let [o, c] = a;
|
|
31
|
+
if (!o || !c) return [...n, {
|
|
30
32
|
publishedAt: null,
|
|
31
33
|
version: null,
|
|
32
|
-
actionName:
|
|
34
|
+
actionName: i,
|
|
33
35
|
sha: null
|
|
34
36
|
}];
|
|
35
37
|
try {
|
|
36
|
-
let
|
|
37
|
-
if (
|
|
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:
|
|
44
|
+
actionName: i,
|
|
43
45
|
sha: null
|
|
44
46
|
}];
|
|
45
|
-
let d = await l.getLatestRelease(
|
|
47
|
+
let d = await l.getLatestRelease(o, c);
|
|
46
48
|
if (!d) {
|
|
47
|
-
let e = await l.getAllReleases(
|
|
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:
|
|
53
|
+
let { publishedAt: a, version: s, sha: u } = d, f = !1;
|
|
52
54
|
{
|
|
53
|
-
let
|
|
54
|
-
f = !
|
|
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
|
|
58
|
-
if (
|
|
59
|
-
let u =
|
|
60
|
-
v: semver.valid(normalizeVersion(
|
|
61
|
-
raw:
|
|
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,
|
|
65
|
-
let
|
|
66
|
-
if (
|
|
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(
|
|
70
|
+
return (/\d+\.\d+/u.test(t.raw.tag) ? 1 : 0) - i;
|
|
69
71
|
});
|
|
70
|
-
let
|
|
71
|
-
if (!
|
|
72
|
-
let
|
|
73
|
-
if (!
|
|
74
|
-
|
|
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 [...
|
|
77
|
-
version:
|
|
78
|
+
return [...n, {
|
|
79
|
+
version: e,
|
|
78
80
|
publishedAt: null,
|
|
79
|
-
sha:
|
|
80
|
-
actionName:
|
|
81
|
+
sha: r,
|
|
82
|
+
actionName: i
|
|
81
83
|
}];
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
|
-
if (!u &&
|
|
87
|
-
u = await l.getTagSha(
|
|
88
|
+
if (!u && s) try {
|
|
89
|
+
u = await l.getTagSha(o, c, s);
|
|
88
90
|
} catch {}
|
|
89
|
-
return [...
|
|
91
|
+
return [...n, {
|
|
90
92
|
status: "ok",
|
|
91
|
-
publishedAt:
|
|
92
|
-
actionName:
|
|
93
|
-
version:
|
|
93
|
+
publishedAt: a,
|
|
94
|
+
actionName: i,
|
|
95
|
+
version: s,
|
|
94
96
|
sha: u
|
|
95
97
|
}];
|
|
96
98
|
}
|
|
97
|
-
let p = await l.getAllTags(
|
|
99
|
+
let p = await l.getAllTags(o, c, 30);
|
|
98
100
|
if (p.length > 0) {
|
|
99
|
-
let
|
|
100
|
-
v: semver.valid(normalizeVersion(
|
|
101
|
-
raw:
|
|
102
|
-
})),
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
if (
|
|
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(
|
|
108
|
-
}),
|
|
109
|
-
let u =
|
|
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(
|
|
113
|
+
d = await l.getTagSha(o, c, u);
|
|
112
114
|
} catch {}
|
|
113
|
-
return [...
|
|
115
|
+
return [...n, {
|
|
114
116
|
status: "ok",
|
|
115
117
|
publishedAt: null,
|
|
116
|
-
actionName:
|
|
118
|
+
actionName: i,
|
|
117
119
|
version: u,
|
|
118
120
|
sha: d
|
|
119
121
|
}];
|
|
120
122
|
}
|
|
121
|
-
return [...
|
|
123
|
+
return [...n, {
|
|
122
124
|
publishedAt: null,
|
|
123
125
|
version: null,
|
|
124
|
-
actionName:
|
|
126
|
+
actionName: i,
|
|
125
127
|
sha: null
|
|
126
128
|
}];
|
|
127
|
-
} catch (
|
|
128
|
-
return
|
|
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:
|
|
133
|
+
actionName: i,
|
|
132
134
|
sha: null
|
|
133
|
-
}]) : (console.warn(`Failed to check ${
|
|
135
|
+
}]) : (console.warn(`Failed to check ${i}:`, e), [...n, {
|
|
134
136
|
publishedAt: null,
|
|
135
137
|
version: null,
|
|
136
|
-
actionName:
|
|
138
|
+
actionName: i,
|
|
137
139
|
sha: null
|
|
138
140
|
}]);
|
|
139
141
|
}
|
|
140
142
|
}), Promise.resolve([]));
|
|
141
143
|
if (p.rateLimitError) {
|
|
142
|
-
let e = !!(
|
|
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(
|
|
173
|
-
let { version:
|
|
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:
|
|
180
|
+
latestVersion: a,
|
|
179
181
|
publishedAt: l,
|
|
180
182
|
skipReason: m,
|
|
181
183
|
latestSha: c,
|
|
182
|
-
action:
|
|
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
|
|
188
|
-
if (
|
|
189
|
-
if (h = semver.lt(
|
|
190
|
-
let
|
|
191
|
-
g = semver.major(
|
|
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(
|
|
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:
|
|
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:
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { isYamlFile } from "./is-yaml-file.js";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import { lstat, readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
async function findYamlFilesRecursive(i) {
|
|
5
5
|
let a = [], o = /* @__PURE__ */ new Set();
|
|
6
6
|
async function s(i) {
|
|
@@ -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";
|
|
8
7
|
import "node:worker_threads";
|
|
9
8
|
import pc from "picocolors";
|
|
10
9
|
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, u = S[t], d = u.display, f = a.action.job ?? "–";
|
|
45
|
+
if (w = Math.max(w, o.length), T = Math.max(T, stripAnsi(d).length, u.versionForPadding && u.shortSha ? stripAnsi(`${padString(u.versionForPadding, D + 1)}${pc.gray(`(${u.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 = [], u = 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 u) {
|
|
69
|
+
let o = !!t.latestSha, u = S[a], d = u.display;
|
|
70
|
+
u.versionForPadding && u.shortSha && (d = `${padString(u.versionForPadding, M + 1)}${pc.gray(`(${u.shortSha})`)}`);
|
|
71
|
+
let f = u.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 } = u[t - 1], s = !!i.latestSha, c = s && !i.isBreaking;
|
|
105
105
|
_.push({
|
|
106
106
|
message: o,
|
|
107
107
|
value: String(a),
|
|
@@ -2,27 +2,26 @@ import { ACTIONS_DIRECTORY, GITHUB_DIRECTORY, WORKFLOWS_DIRECTORY } from "./cons
|
|
|
2
2
|
import { isYamlFile } from "./fs/is-yaml-file.js";
|
|
3
3
|
import { scanWorkflowFile } from "./scan-workflow-file.js";
|
|
4
4
|
import { scanActionFile } from "./scan-action-file.js";
|
|
5
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
5
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
|
-
|
|
6
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
+
async function scanGitHubActions(d = 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(d);
|
|
13
|
+
function _(e, o) {
|
|
14
|
+
let s = relative(e, o);
|
|
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
|
}
|
|
22
21
|
async function b(e) {
|
|
23
22
|
try {
|
|
24
|
-
let
|
|
25
|
-
return typeof
|
|
23
|
+
let o = await stat(e);
|
|
24
|
+
return typeof o.isFile == "function" ? o.isFile() : !1;
|
|
26
25
|
} catch {
|
|
27
26
|
return !1;
|
|
28
27
|
}
|
|
@@ -31,13 +30,13 @@ async function scanGitHubActions(p = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
31
30
|
try {
|
|
32
31
|
if ((await stat(x)).isDirectory()) {
|
|
33
32
|
let e = (await readdir(x)).filter((e) => y(e) ? isYamlFile(e) : !1).map(async (e) => {
|
|
34
|
-
let
|
|
33
|
+
let o = join(x, e);
|
|
35
34
|
try {
|
|
36
|
-
let
|
|
35
|
+
let c = await scanWorkflowFile(o);
|
|
37
36
|
return {
|
|
38
37
|
path: `${m}/${WORKFLOWS_DIRECTORY}/${e}`,
|
|
39
38
|
success: !0,
|
|
40
|
-
actions:
|
|
39
|
+
actions: c
|
|
41
40
|
};
|
|
42
41
|
} catch {
|
|
43
42
|
return {
|
|
@@ -46,111 +45,111 @@ async function scanGitHubActions(p = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
46
45
|
actions: []
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
|
-
}),
|
|
50
|
-
for (let e of
|
|
48
|
+
}), o = await Promise.all(e);
|
|
49
|
+
for (let e of o) 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
50
|
}
|
|
52
51
|
} catch {}
|
|
53
52
|
try {
|
|
54
|
-
let e = join(g, "action.yml"),
|
|
53
|
+
let e = join(g, "action.yml"), o = join(g, "action.yaml"), s = null, c = [];
|
|
55
54
|
if (await b(e)) try {
|
|
56
|
-
|
|
55
|
+
c = await scanActionFile(e), s = e;
|
|
57
56
|
} catch {
|
|
58
|
-
|
|
57
|
+
s = null;
|
|
59
58
|
}
|
|
60
|
-
if (!
|
|
61
|
-
|
|
59
|
+
if (!s && await b(o)) try {
|
|
60
|
+
c = await scanActionFile(o), s = o;
|
|
62
61
|
} catch {
|
|
63
|
-
|
|
62
|
+
s = null;
|
|
64
63
|
}
|
|
65
|
-
if (
|
|
66
|
-
let e = relative(g,
|
|
67
|
-
h.compositeActions.set(e, e),
|
|
64
|
+
if (s) {
|
|
65
|
+
let e = relative(g, s);
|
|
66
|
+
h.compositeActions.set(e, e), c.length > 0 && h.actions.push(...c);
|
|
68
67
|
}
|
|
69
68
|
} catch {}
|
|
70
69
|
let S = join(v, ACTIONS_DIRECTORY);
|
|
71
70
|
try {
|
|
72
71
|
if ((await stat(S)).isDirectory()) {
|
|
73
|
-
let
|
|
74
|
-
if (!y(
|
|
75
|
-
let
|
|
72
|
+
let o = (await readdir(S)).map(async (o) => {
|
|
73
|
+
if (!y(o)) return null;
|
|
74
|
+
let s = join(S, o);
|
|
76
75
|
try {
|
|
77
|
-
if (!(await stat(
|
|
78
|
-
let
|
|
76
|
+
if (!(await stat(s)).isDirectory()) return null;
|
|
77
|
+
let c = join(s, "action.yml"), l = [];
|
|
79
78
|
try {
|
|
80
|
-
|
|
79
|
+
l = await scanActionFile(c);
|
|
81
80
|
} catch {
|
|
82
81
|
try {
|
|
83
|
-
|
|
82
|
+
c = join(s, "action.yaml"), l = await scanActionFile(c);
|
|
84
83
|
} catch {
|
|
85
84
|
return null;
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
return {
|
|
89
|
-
path: `${m}/${ACTIONS_DIRECTORY}/${
|
|
90
|
-
name:
|
|
91
|
-
actions:
|
|
88
|
+
path: `${m}/${ACTIONS_DIRECTORY}/${o}`,
|
|
89
|
+
name: o,
|
|
90
|
+
actions: l
|
|
92
91
|
};
|
|
93
92
|
} catch {
|
|
94
93
|
return null;
|
|
95
94
|
}
|
|
96
|
-
}),
|
|
97
|
-
for (let e of
|
|
95
|
+
}), s = await Promise.all(o);
|
|
96
|
+
for (let e of s) e && (h.compositeActions.set(e.name, e.path), h.actions.push(...e.actions));
|
|
98
97
|
}
|
|
99
98
|
} catch {}
|
|
100
99
|
try {
|
|
101
100
|
let e = await getCurrentRepoSlug(g);
|
|
102
101
|
if (e) {
|
|
103
102
|
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,
|
|
103
|
+
let o = /* @__PURE__ */ new Set(), s = [];
|
|
104
|
+
for (let c of h.actions) {
|
|
105
|
+
if (c.type !== "external") continue;
|
|
106
|
+
let l = c.name.split("/");
|
|
107
|
+
if (l.length < 3 || `${l[0]}/${l[1]}` !== e) continue;
|
|
108
|
+
let u = join(g, ...l.slice(2));
|
|
109
|
+
_(g, u) && (o.has(u) || (o.add(u), s.push(u)));
|
|
111
110
|
}
|
|
112
|
-
async function
|
|
113
|
-
if (
|
|
114
|
-
let
|
|
111
|
+
async function c() {
|
|
112
|
+
if (s.length === 0) return;
|
|
113
|
+
let l = s.splice(0), d = await Promise.all(l.map(async (s) => {
|
|
115
114
|
try {
|
|
116
|
-
let
|
|
115
|
+
let c = join(s, "action.yml"), l = join(s, "action.yaml"), d = c;
|
|
117
116
|
try {
|
|
118
|
-
if (!(await stat(s)).isFile()) throw Error("not a file");
|
|
119
|
-
} catch {
|
|
120
117
|
if (!(await stat(c)).isFile()) throw Error("not a file");
|
|
121
|
-
|
|
118
|
+
} catch {
|
|
119
|
+
if (!(await stat(l)).isFile()) throw Error("not a file");
|
|
120
|
+
d = l;
|
|
122
121
|
}
|
|
123
|
-
let
|
|
124
|
-
|
|
125
|
-
let
|
|
126
|
-
for (let
|
|
127
|
-
if (
|
|
128
|
-
let
|
|
129
|
-
if (
|
|
130
|
-
let
|
|
131
|
-
_(g,
|
|
122
|
+
let f = await scanActionFile(d);
|
|
123
|
+
f.length > 0 && h.actions.push(...f);
|
|
124
|
+
let p = [];
|
|
125
|
+
for (let s of f) {
|
|
126
|
+
if (s.type !== "external") continue;
|
|
127
|
+
let c = s.name.split("/");
|
|
128
|
+
if (c.length < 3 || `${c[0]}/${c[1]}` !== e) continue;
|
|
129
|
+
let l = join(g, ...c.slice(2));
|
|
130
|
+
_(g, l) && (o.has(l) || (o.add(l), p.push(l)));
|
|
132
131
|
}
|
|
133
|
-
return
|
|
132
|
+
return p;
|
|
134
133
|
} catch {
|
|
135
134
|
return [];
|
|
136
135
|
}
|
|
137
136
|
}));
|
|
138
|
-
for (let e of
|
|
139
|
-
await
|
|
137
|
+
for (let e of d) for (let o of e) s.push(o);
|
|
138
|
+
await c();
|
|
140
139
|
}
|
|
141
|
-
await
|
|
140
|
+
await c();
|
|
142
141
|
}
|
|
143
142
|
} catch {}
|
|
144
143
|
return h;
|
|
145
144
|
}
|
|
146
145
|
async function getCurrentRepoSlug(e) {
|
|
147
|
-
let
|
|
148
|
-
if (
|
|
146
|
+
let o = process.env.GITHUB_REPOSITORY;
|
|
147
|
+
if (o && /^[^\s/]+\/[^\s/]+$/u.test(o)) return o;
|
|
149
148
|
try {
|
|
150
|
-
let
|
|
151
|
-
if (
|
|
152
|
-
let
|
|
153
|
-
if (
|
|
149
|
+
let o = await readFile(join(e, ".git", "config"), "utf8"), s = o.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim();
|
|
150
|
+
if (s ||= o.match(/url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim(), !s) return null;
|
|
151
|
+
let c = s.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^./]+)(?:\.git)?$/u);
|
|
152
|
+
if (c?.groups) return `${c.groups.owner}/${c.groups.repo}`;
|
|
154
153
|
} catch {}
|
|
155
154
|
return null;
|
|
156
155
|
}
|
|
@@ -4,22 +4,20 @@ import { isWorkflowStructure } from "./schema/workflow/is-workflow-structure.js"
|
|
|
4
4
|
import { findYamlFilesRecursive } from "./fs/find-yaml-files-recursive.js";
|
|
5
5
|
import { scanWorkflowAst } from "./ast/scanners/scan-workflow-ast.js";
|
|
6
6
|
import { readYamlDocument } from "./fs/read-yaml-document.js";
|
|
7
|
-
import { dirname,
|
|
8
|
-
async function scanRecursive(
|
|
9
|
-
let
|
|
7
|
+
import { dirname, relative, resolve } from "node:path";
|
|
8
|
+
async function scanRecursive(u, d) {
|
|
9
|
+
let f = {
|
|
10
10
|
compositeActions: /* @__PURE__ */ new Map(),
|
|
11
11
|
workflows: /* @__PURE__ */ new Map(),
|
|
12
12
|
actions: []
|
|
13
|
-
},
|
|
14
|
-
if (g !== "" && (g.startsWith("..") || isAbsolute(g))) throw Error("Invalid path: detected path traversal attempt");
|
|
15
|
-
let _;
|
|
13
|
+
}, p = resolve(u), m = resolve(p, d), h;
|
|
16
14
|
try {
|
|
17
|
-
|
|
15
|
+
h = await findYamlFilesRecursive(m);
|
|
18
16
|
} catch {
|
|
19
|
-
return
|
|
17
|
+
return f;
|
|
20
18
|
}
|
|
21
|
-
let
|
|
22
|
-
let s = relative(
|
|
19
|
+
let g = h.map(async (o) => {
|
|
20
|
+
let s = relative(p, o);
|
|
23
21
|
try {
|
|
24
22
|
let { document: c, content: l } = await readYamlDocument(o), u = c.toJSON();
|
|
25
23
|
if (isWorkflowStructure(u) && hasKey(u, "jobs")) return {
|
|
@@ -34,13 +32,13 @@ async function scanRecursive(d, f) {
|
|
|
34
32
|
};
|
|
35
33
|
} catch {}
|
|
36
34
|
return null;
|
|
37
|
-
}),
|
|
38
|
-
for (let e of
|
|
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);
|
|
39
37
|
else {
|
|
40
38
|
let i = dirname(e.path), a = i === "." || i === "" ? e.path : i;
|
|
41
|
-
|
|
39
|
+
f.compositeActions.set(a, e.path), f.actions.push(...e.actions);
|
|
42
40
|
}
|
|
43
|
-
return
|
|
41
|
+
return f;
|
|
44
42
|
}
|
|
45
43
|
function hasKey(e, i) {
|
|
46
44
|
return typeof e == "object" && !!e && i in e;
|
|
@@ -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,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;
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "1.
|
|
1
|
+
const version = "1.12.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -146,9 +146,12 @@ npx actions-up --dir .github --dir ./other/.github
|
|
|
146
146
|
Use `--recursive` (`-r`) to scan YAML workflow/composite-action files recursively in the selected directories:
|
|
147
147
|
|
|
148
148
|
```bash
|
|
149
|
+
npx actions-up -r
|
|
149
150
|
npx actions-up --dir ./gh-repo-defaults -r
|
|
150
151
|
```
|
|
151
152
|
|
|
153
|
+
When `--recursive` is used without `--dir`, Actions Up scans from the current directory (`.`).
|
|
154
|
+
|
|
152
155
|
### Branch References
|
|
153
156
|
|
|
154
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`.
|
|
@@ -162,6 +165,10 @@ npx actions-up --mode minor
|
|
|
162
165
|
npx actions-up --mode patch
|
|
163
166
|
```
|
|
164
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
|
+
|
|
165
172
|
## GitHub Actions Integration
|
|
166
173
|
|
|
167
174
|
### Automated PR Checks
|