actions-up 1.5.0 → 1.7.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 +38 -27
- package/dist/core/api/check-updates.d.ts +4 -1
- package/dist/core/api/check-updates.js +145 -102
- package/dist/core/ast/scanners/scan-composite-action-ast.js +5 -1
- package/dist/core/ast/scanners/scan-workflow-ast.d.ts +2 -1
- package/dist/core/ast/scanners/scan-workflow-ast.js +22 -9
- package/dist/core/ast/utils/extract-uses-from-steps.d.ts +13 -4
- package/dist/core/ast/utils/extract-uses-from-steps.js +10 -9
- package/dist/core/interactive/prompt-update-selection.d.ts +6 -1
- package/dist/core/interactive/prompt-update-selection.js +122 -94
- package/dist/core/parsing/parse-action-reference.js +1 -1
- package/dist/package.js +1 -1
- package/dist/types/action-update.d.ts +9 -0
- package/dist/types/github-action.d.ts +4 -1
- package/dist/types/workflow-job.d.ts +9 -0
- package/package.json +2 -2
- package/readme.md +38 -0
package/dist/cli/index.js
CHANGED
|
@@ -10,55 +10,57 @@ import "node:worker_threads";
|
|
|
10
10
|
import pc from "picocolors";
|
|
11
11
|
import cac from "cac";
|
|
12
12
|
function run() {
|
|
13
|
-
let
|
|
14
|
-
|
|
13
|
+
let u = cac("actions-up");
|
|
14
|
+
u.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("--yes, -y", "Skip all confirmations").command("", "Update GitHub Actions").action(async (c) => {
|
|
15
15
|
console.info(pc.cyan("\n🚀 Actions Up!\n"));
|
|
16
|
-
let
|
|
16
|
+
let l = createSpinner("Scanning GitHub Actions...").start();
|
|
17
17
|
try {
|
|
18
|
-
let
|
|
19
|
-
if (
|
|
18
|
+
let u = await scanGitHubActions(process.cwd(), c.dir), d = u.actions.length, f = u.workflows.size, p = u.compositeActions.size;
|
|
19
|
+
if (l.success(`Found ${pc.yellow(d)} actions in ${pc.yellow(f)} workflows and ${pc.yellow(p)} composite actions`), d === 0) {
|
|
20
20
|
console.info(pc.green("\n✨ No GitHub Actions found in this repository"));
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
let
|
|
24
|
-
Array.isArray(
|
|
25
|
-
let
|
|
26
|
-
if (
|
|
27
|
-
let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"), a = e(
|
|
28
|
-
a.length > 0 && (
|
|
23
|
+
let m = u.actions, h = [];
|
|
24
|
+
Array.isArray(c.exclude) ? h.push(...c.exclude) : typeof c.exclude == "string" && h.push(c.exclude);
|
|
25
|
+
let g = h.flatMap((e) => e.split(",")).map((e) => e.trim()).filter(Boolean);
|
|
26
|
+
if (g.length > 0) {
|
|
27
|
+
let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"), a = e(g);
|
|
28
|
+
a.length > 0 && (m = m.filter((e) => {
|
|
29
29
|
let { name: o } = e;
|
|
30
30
|
for (let e of a) if (e.test(o)) return !1;
|
|
31
31
|
return !0;
|
|
32
32
|
}));
|
|
33
33
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
34
|
+
if (l = createSpinner("Checking for updates...").start(), m.length === 0) {
|
|
35
|
+
l.success("No actions to check after excludes"), console.info(pc.green("\n✨ Nothing to check after excludes\n"));
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
let
|
|
39
|
-
await Promise.all(
|
|
40
|
-
await shouldIgnore(e.action.file, e.action.line) ||
|
|
38
|
+
let _ = c.includeBranches ?? !1, v = await checkUpdates(m, process.env.GITHUB_TOKEN, { includeBranches: _ }), y = [];
|
|
39
|
+
await Promise.all(v.map(async (e) => {
|
|
40
|
+
await shouldIgnore(e.action.file, e.action.line) || y.push(e);
|
|
41
41
|
}));
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
let b = y.filter((e) => e.status === "skipped"), x = y.filter((e) => e.hasUpdate), S = c.minAge * 24 * 60 * 60 * 1e3, C = Date.now();
|
|
43
|
+
x = x.filter((e) => e.publishedAt ? C - e.publishedAt.getTime() >= S : !0);
|
|
44
|
+
let w = x.filter((e) => e.isBreaking);
|
|
45
|
+
if (x.length === 0) {
|
|
46
|
+
l.success("All actions are up to date!"), b.length > 0 && printSkippedWarning(b, _), console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
|
-
if (
|
|
49
|
+
if (l.success(`Found ${pc.yellow(x.length)} updates available${w.length > 0 ? ` (${pc.redBright(w.length)} breaking)` : ""}`), b.length > 0 && printSkippedWarning(b, _), c.dryRun) {
|
|
48
50
|
console.info(pc.yellow("\n📋 Dry Run - No changes will be made\n"));
|
|
49
|
-
for (let e of
|
|
50
|
-
console.info(pc.gray(`\n${
|
|
51
|
+
for (let e of x) 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`);
|
|
52
|
+
console.info(pc.gray(`\n${x.length} actions would be updated\n`));
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
|
-
if (
|
|
54
|
-
let e =
|
|
55
|
+
if (c.yes) {
|
|
56
|
+
let e = x.filter((e) => e.latestSha);
|
|
55
57
|
if (e.length === 0) {
|
|
56
58
|
console.info(pc.yellow("\n⚠️ No actions with SHA available for update\n"));
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
61
|
console.info(pc.yellow(`\n🔄 Updating ${e.length} actions...\n`)), await applyUpdates(e), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
60
62
|
} else {
|
|
61
|
-
let o = await promptUpdateSelection(
|
|
63
|
+
let o = await promptUpdateSelection(y, { showAge: c.minAge > 0 });
|
|
62
64
|
if (!o || o.length === 0) {
|
|
63
65
|
console.info(pc.gray("\nNo updates applied"));
|
|
64
66
|
return;
|
|
@@ -66,8 +68,17 @@ function run() {
|
|
|
66
68
|
console.info(pc.yellow(`\n🔄 Updating ${o.length} selected actions...\n`)), await applyUpdates(o), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
67
69
|
}
|
|
68
70
|
} catch (e) {
|
|
69
|
-
|
|
71
|
+
l.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);
|
|
70
72
|
}
|
|
71
|
-
}),
|
|
73
|
+
}), u.parse();
|
|
74
|
+
}
|
|
75
|
+
function printSkippedWarning(e, a) {
|
|
76
|
+
let o = new Intl.PluralRules("en-US", { type: "cardinal" }).select(e.length) === "one" ? "action" : "actions", s = a ? "" : " (use --include-branches to check them)";
|
|
77
|
+
console.info(pc.yellow(`\n⚠️ Skipped ${e.length} ${o} pinned to branches${s}`));
|
|
78
|
+
for (let a of e) {
|
|
79
|
+
let e = a.action.uses ?? `${a.action.name}@${a.currentVersion ?? "unknown"}`;
|
|
80
|
+
console.info(pc.gray(` • ${e}`));
|
|
81
|
+
}
|
|
82
|
+
console.info("");
|
|
72
83
|
}
|
|
73
84
|
export { run };
|
|
@@ -5,6 +5,9 @@ import { ActionUpdate } from '../../types/action-update';
|
|
|
5
5
|
*
|
|
6
6
|
* @param actions - Array of GitHub Actions to check.
|
|
7
7
|
* @param token - Optional GitHub token for authentication.
|
|
8
|
+
* @param options - Additional options (e.g., include branch refs).
|
|
8
9
|
* @returns Array of update information.
|
|
9
10
|
*/
|
|
10
|
-
export declare function checkUpdates(actions: GitHubAction[], token?: string
|
|
11
|
+
export declare function checkUpdates(actions: GitHubAction[], token?: string, options?: {
|
|
12
|
+
includeBranches?: boolean;
|
|
13
|
+
}): Promise<ActionUpdate[]>;
|
|
@@ -1,186 +1,229 @@
|
|
|
1
1
|
import { createGitHubClient } from "./create-github-client.js";
|
|
2
2
|
import semver from "semver";
|
|
3
|
-
async function checkUpdates(
|
|
4
|
-
let
|
|
5
|
-
if (
|
|
6
|
-
let
|
|
7
|
-
for (let e of
|
|
8
|
-
let
|
|
9
|
-
|
|
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
|
+
if (d.length === 0) return [];
|
|
6
|
+
let f = /* @__PURE__ */ new Map();
|
|
7
|
+
for (let e of d) {
|
|
8
|
+
let t = f.get(e.name) ?? [];
|
|
9
|
+
t.push(e), f.set(e.name, t);
|
|
10
10
|
}
|
|
11
|
-
let
|
|
11
|
+
let p = {
|
|
12
12
|
rateLimitError: null,
|
|
13
13
|
rateLimitHit: !1
|
|
14
|
-
},
|
|
15
|
-
if (
|
|
14
|
+
}, m = await [...f.keys()].reduce((e, n) => e.then(async (e) => {
|
|
15
|
+
if (p.rateLimitHit) return [...e, {
|
|
16
|
+
publishedAt: null,
|
|
16
17
|
version: null,
|
|
17
|
-
actionName:
|
|
18
|
+
actionName: n,
|
|
18
19
|
sha: null
|
|
19
20
|
}];
|
|
20
|
-
let
|
|
21
|
-
if (
|
|
21
|
+
let r = n.split("/");
|
|
22
|
+
if (r.length < 2) return [...e, {
|
|
23
|
+
publishedAt: null,
|
|
22
24
|
version: null,
|
|
23
|
-
actionName:
|
|
25
|
+
actionName: n,
|
|
24
26
|
sha: null
|
|
25
27
|
}];
|
|
26
|
-
let [
|
|
27
|
-
if (!
|
|
28
|
+
let [i, c] = r;
|
|
29
|
+
if (!i || !c) return [...e, {
|
|
30
|
+
publishedAt: null,
|
|
28
31
|
version: null,
|
|
29
|
-
actionName:
|
|
32
|
+
actionName: n,
|
|
30
33
|
sha: null
|
|
31
34
|
}];
|
|
32
35
|
try {
|
|
33
|
-
let
|
|
34
|
-
if (
|
|
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
|
+
skipReason: "branch",
|
|
39
|
+
status: "skipped",
|
|
40
|
+
publishedAt: null,
|
|
35
41
|
version: null,
|
|
36
|
-
actionName:
|
|
42
|
+
actionName: n,
|
|
37
43
|
sha: null
|
|
38
44
|
}];
|
|
39
|
-
let d = await
|
|
45
|
+
let d = await l.getLatestRelease(i, c);
|
|
40
46
|
if (!d) {
|
|
41
|
-
let e = await
|
|
47
|
+
let e = await l.getAllReleases(i, c, 1);
|
|
42
48
|
d = e.find((e) => !e.isPrerelease) ?? e[0] ?? null;
|
|
43
49
|
}
|
|
44
50
|
if (d) {
|
|
45
|
-
let { version:
|
|
51
|
+
let { publishedAt: r, version: o, sha: u } = d, f = !1;
|
|
46
52
|
{
|
|
47
|
-
let e = normalizeVersion(
|
|
48
|
-
|
|
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;
|
|
49
55
|
}
|
|
50
|
-
if (
|
|
51
|
-
let
|
|
52
|
-
if (
|
|
53
|
-
let u =
|
|
56
|
+
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) => ({
|
|
54
60
|
v: semver.valid(normalizeVersion(e.tag)),
|
|
55
61
|
raw: e
|
|
56
62
|
}));
|
|
57
63
|
if (u.length > 0) {
|
|
58
|
-
u.sort((e,
|
|
59
|
-
let
|
|
60
|
-
if (
|
|
61
|
-
let
|
|
62
|
-
return (/\d+\.\d+/u.test(
|
|
64
|
+
u.sort((e, n) => {
|
|
65
|
+
let r = semver.rcompare(e.v, n.v);
|
|
66
|
+
if (r !== 0) return r;
|
|
67
|
+
let i = /\d+\.\d+/u.test(e.raw.tag) ? 1 : 0;
|
|
68
|
+
return (/\d+\.\d+/u.test(n.raw.tag) ? 1 : 0) - i;
|
|
63
69
|
});
|
|
64
|
-
let
|
|
65
|
-
if (!s || semver.gt(u[0].v, s) || semver.eq(u[0].v, s) && /\d+\.\d+/u.test(
|
|
66
|
-
let
|
|
67
|
-
if (!
|
|
68
|
-
|
|
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);
|
|
69
75
|
} catch {}
|
|
70
76
|
return [...e, {
|
|
71
|
-
version:
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
version: t,
|
|
78
|
+
publishedAt: null,
|
|
79
|
+
sha: a,
|
|
80
|
+
actionName: n
|
|
74
81
|
}];
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
|
-
if (!
|
|
80
|
-
|
|
86
|
+
if (!u && o) try {
|
|
87
|
+
u = await l.getTagSha(i, c, o);
|
|
81
88
|
} catch {}
|
|
82
89
|
return [...e, {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
status: "ok",
|
|
91
|
+
publishedAt: r,
|
|
92
|
+
actionName: n,
|
|
93
|
+
version: o,
|
|
94
|
+
sha: u
|
|
86
95
|
}];
|
|
87
96
|
}
|
|
88
|
-
let
|
|
89
|
-
if (
|
|
90
|
-
let
|
|
97
|
+
let p = await l.getAllTags(i, c, 30);
|
|
98
|
+
if (p.length > 0) {
|
|
99
|
+
let r = p.filter((e) => isSemverLike(e.tag)).map((e) => ({
|
|
91
100
|
v: semver.valid(normalizeVersion(e.tag)),
|
|
92
101
|
raw: e
|
|
93
102
|
})), o;
|
|
94
|
-
|
|
95
|
-
let
|
|
96
|
-
if (
|
|
97
|
-
let
|
|
98
|
-
return (/\d+\.\d+/u.test(
|
|
99
|
-
}), o =
|
|
103
|
+
r.length > 0 ? (r.sort((e, n) => {
|
|
104
|
+
let r = semver.rcompare(e.v, n.v);
|
|
105
|
+
if (r !== 0) return r;
|
|
106
|
+
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];
|
|
100
109
|
let u = o.tag, d = o.sha?.length ? o.sha : null;
|
|
101
110
|
if (!d && u) try {
|
|
102
|
-
d = await
|
|
111
|
+
d = await l.getTagSha(i, c, u);
|
|
103
112
|
} catch {}
|
|
104
113
|
return [...e, {
|
|
105
|
-
|
|
114
|
+
status: "ok",
|
|
115
|
+
publishedAt: null,
|
|
116
|
+
actionName: n,
|
|
106
117
|
version: u,
|
|
107
118
|
sha: d
|
|
108
119
|
}];
|
|
109
120
|
}
|
|
110
121
|
return [...e, {
|
|
122
|
+
publishedAt: null,
|
|
111
123
|
version: null,
|
|
112
|
-
actionName:
|
|
124
|
+
actionName: n,
|
|
113
125
|
sha: null
|
|
114
126
|
}];
|
|
115
|
-
} catch (
|
|
116
|
-
return
|
|
127
|
+
} catch (t) {
|
|
128
|
+
return t instanceof Error && t.name === "GitHubRateLimitError" ? (p.rateLimitHit = !0, p.rateLimitError = t, [...e, {
|
|
129
|
+
publishedAt: null,
|
|
117
130
|
version: null,
|
|
118
|
-
actionName:
|
|
131
|
+
actionName: n,
|
|
119
132
|
sha: null
|
|
120
|
-
}]) : (console.warn(`Failed to check ${
|
|
133
|
+
}]) : (console.warn(`Failed to check ${n}:`, t), [...e, {
|
|
134
|
+
publishedAt: null,
|
|
121
135
|
version: null,
|
|
122
|
-
actionName:
|
|
136
|
+
actionName: n,
|
|
123
137
|
sha: null
|
|
124
138
|
}]);
|
|
125
139
|
}
|
|
126
140
|
}), Promise.resolve([]));
|
|
127
|
-
if (
|
|
128
|
-
let e = !!(
|
|
129
|
-
throw
|
|
141
|
+
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);
|
|
143
|
+
throw n.name = "GitHubRateLimitError", n;
|
|
130
144
|
}
|
|
131
|
-
let
|
|
132
|
-
for (let e of
|
|
145
|
+
let h = /* @__PURE__ */ new Map();
|
|
146
|
+
for (let e of m) h.set(e.actionName, {
|
|
147
|
+
publishedAt: e.publishedAt,
|
|
148
|
+
actionName: e.actionName,
|
|
149
|
+
skipReason: e.skipReason,
|
|
133
150
|
version: e.version,
|
|
151
|
+
status: e.status,
|
|
134
152
|
sha: e.sha
|
|
135
153
|
});
|
|
136
|
-
let
|
|
137
|
-
for (let e of
|
|
138
|
-
let
|
|
139
|
-
|
|
154
|
+
let g = [];
|
|
155
|
+
for (let e of d) {
|
|
156
|
+
let t = h.get(e.name);
|
|
157
|
+
t ? g.push(createUpdate(e, {
|
|
158
|
+
publishedAt: t.publishedAt,
|
|
159
|
+
version: t.version,
|
|
160
|
+
sha: t.sha
|
|
161
|
+
}, {
|
|
162
|
+
skipReason: t.skipReason,
|
|
163
|
+
status: t.status
|
|
164
|
+
})) : g.push(createUpdate(e, {
|
|
165
|
+
publishedAt: null,
|
|
166
|
+
version: null,
|
|
167
|
+
sha: null
|
|
168
|
+
}));
|
|
140
169
|
}
|
|
141
|
-
return
|
|
170
|
+
return g;
|
|
142
171
|
}
|
|
143
|
-
function createUpdate(e,
|
|
144
|
-
let s =
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
if (p === "skipped") return {
|
|
175
|
+
currentVersion: u,
|
|
176
|
+
isBreaking: !1,
|
|
177
|
+
hasUpdate: !1,
|
|
178
|
+
latestVersion: s,
|
|
179
|
+
publishedAt: l,
|
|
180
|
+
skipReason: m,
|
|
181
|
+
latestSha: c,
|
|
182
|
+
action: e,
|
|
183
|
+
status: p
|
|
184
|
+
};
|
|
185
|
+
if (d && isSha(d)) c ? h = !compareSha(d, c) : f && (h = !0);
|
|
186
|
+
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;
|
|
152
192
|
}
|
|
153
|
-
!
|
|
154
|
-
} else
|
|
193
|
+
!h && semver.eq(n, r) && !isSha(e.version) && c && (h = !0, g = !1);
|
|
194
|
+
} else d !== f && (h = !0);
|
|
155
195
|
}
|
|
156
196
|
return {
|
|
157
|
-
currentVersion:
|
|
158
|
-
latestVersion:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
197
|
+
currentVersion: u,
|
|
198
|
+
latestVersion: s,
|
|
199
|
+
publishedAt: l,
|
|
200
|
+
isBreaking: g,
|
|
201
|
+
skipReason: m,
|
|
202
|
+
latestSha: c,
|
|
203
|
+
hasUpdate: h,
|
|
204
|
+
action: e,
|
|
205
|
+
status: p
|
|
163
206
|
};
|
|
164
207
|
}
|
|
165
|
-
function compareSha(e,
|
|
166
|
-
let
|
|
167
|
-
return
|
|
208
|
+
function compareSha(e, t) {
|
|
209
|
+
let n = e.replace(/^v/u, ""), r = t.replace(/^v/u, ""), i = Math.min(n.length, r.length);
|
|
210
|
+
return i < 7 ? !1 : n.slice(0, Math.max(0, i)).toLowerCase() === r.slice(0, Math.max(0, i)).toLowerCase();
|
|
168
211
|
}
|
|
169
212
|
function normalizeVersion(e) {
|
|
170
213
|
if (!e) return null;
|
|
171
|
-
let
|
|
172
|
-
if (/^[0-9a-f]{7,40}$/iu.test(
|
|
173
|
-
let
|
|
174
|
-
return
|
|
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;
|
|
175
218
|
}
|
|
176
219
|
function isSha(e) {
|
|
177
220
|
if (!e) return !1;
|
|
178
|
-
let
|
|
179
|
-
return /^[0-9a-f]{7,40}$/iu.test(
|
|
221
|
+
let t = e.replace(/^v/u, "");
|
|
222
|
+
return /^[0-9a-f]{7,40}$/iu.test(t);
|
|
180
223
|
}
|
|
181
224
|
function isSemverLike(e) {
|
|
182
225
|
if (!e) return !1;
|
|
183
|
-
let
|
|
184
|
-
return /^v?\d+(?:\.\d+){0,2}$/u.test(
|
|
226
|
+
let t = e.trim();
|
|
227
|
+
return /^v?\d+(?:\.\d+){0,2}$/u.test(t);
|
|
185
228
|
}
|
|
186
229
|
export { checkUpdates };
|
|
@@ -11,6 +11,10 @@ function scanCompositeActionAst(a, o, s) {
|
|
|
11
11
|
let u = c.runs;
|
|
12
12
|
if (!u || !isCompositeActionRuns(u) || !u.steps || !Array.isArray(u.steps)) return [];
|
|
13
13
|
let d = findMapPair(l.value, "steps");
|
|
14
|
-
return d?.value ? extractUsesFromSteps(
|
|
14
|
+
return d?.value ? extractUsesFromSteps({
|
|
15
|
+
stepsNode: d.value,
|
|
16
|
+
filePath: s,
|
|
17
|
+
content: o
|
|
18
|
+
}) : [];
|
|
15
19
|
}
|
|
16
20
|
export { scanCompositeActionAst };
|
|
@@ -4,7 +4,8 @@ import { GitHubAction } from '../../../types/github-action';
|
|
|
4
4
|
* Scans a parsed workflow YAML document for action references.
|
|
5
5
|
*
|
|
6
6
|
* Navigates AST structure `jobs -> <job> -> steps` and extracts `uses` entries
|
|
7
|
-
* with corresponding line numbers.
|
|
7
|
+
* with corresponding line numbers. Also scans for job-level `uses` fields that
|
|
8
|
+
* indicate Reusable Workflows.
|
|
8
9
|
*
|
|
9
10
|
* @param document - Parsed YAML document of a workflow file.
|
|
10
11
|
* @param content - Original file content.
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { isWorkflowStructure } from "../../schema/workflow/is-workflow-structure.js";
|
|
2
|
+
import { parseActionReference } from "../../parsing/parse-action-reference.js";
|
|
3
|
+
import { getLineNumberForKey } from "../utils/get-line-number.js";
|
|
2
4
|
import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
5
|
+
import { isScalar } from "../guards/is-scalar.js";
|
|
3
6
|
import { isNode } from "../guards/is-node.js";
|
|
4
7
|
import { isPair } from "../guards/is-pair.js";
|
|
5
8
|
import { extractUsesFromSteps } from "../utils/extract-uses-from-steps.js";
|
|
6
9
|
import { findMapPair } from "../utils/find-map-pair.js";
|
|
7
|
-
function scanWorkflowAst(
|
|
8
|
-
if (!isWorkflowStructure(
|
|
9
|
-
let
|
|
10
|
-
if (!
|
|
11
|
-
let
|
|
12
|
-
for (let e of
|
|
10
|
+
function scanWorkflowAst(l, u, d) {
|
|
11
|
+
if (!isWorkflowStructure(l.toJSON()) || !l.contents || !isYAMLMap(l.contents)) return [];
|
|
12
|
+
let f = findMapPair(l.contents, "jobs");
|
|
13
|
+
if (!f?.value || !isYAMLMap(f.value)) return [];
|
|
14
|
+
let p = [];
|
|
15
|
+
for (let e of f.value.items) {
|
|
13
16
|
if (!isPair(e) || !e.value || !isNode(e.value) || !isYAMLMap(e.value)) continue;
|
|
14
|
-
let
|
|
15
|
-
|
|
17
|
+
let l = isScalar(e.key) ? String(e.key.value) : void 0, f = findMapPair(e.value, "uses");
|
|
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));
|
|
21
|
+
}
|
|
22
|
+
let m = findMapPair(e.value, "steps");
|
|
23
|
+
m?.value && p.push(...extractUsesFromSteps({
|
|
24
|
+
stepsNode: m.value,
|
|
25
|
+
filePath: d,
|
|
26
|
+
content: u,
|
|
27
|
+
jobName: l
|
|
28
|
+
}));
|
|
16
29
|
}
|
|
17
|
-
return
|
|
30
|
+
return p;
|
|
18
31
|
}
|
|
19
32
|
export { scanWorkflowAst };
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { GitHubAction } from '../../../types/github-action';
|
|
2
|
+
interface ExtractUsesOptions {
|
|
3
|
+
/** YAML sequence node containing workflow/action steps. */
|
|
4
|
+
stepsNode: unknown;
|
|
5
|
+
/** Path of the file being scanned (for metadata). */
|
|
6
|
+
filePath: string;
|
|
7
|
+
/** Name of the job containing these steps (for workflows). */
|
|
8
|
+
jobName?: string;
|
|
9
|
+
/** Original YAML file content (for line number calculation). */
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Extracts GitHub Action references from a steps YAML sequence.
|
|
4
14
|
*
|
|
5
15
|
* Uses the AST to locate the 'uses' key for precise line numbers and the JSON
|
|
6
16
|
* representation to validate the presence and type of the 'uses' field.
|
|
7
17
|
*
|
|
8
|
-
* @param
|
|
9
|
-
* @param filePath - Path of the file being scanned (for metadata).
|
|
10
|
-
* @param content - Original YAML file content (for line number calculation).
|
|
18
|
+
* @param options - Options for extraction.
|
|
11
19
|
* @returns List of discovered GitHub actions.
|
|
12
20
|
*/
|
|
13
|
-
export declare function extractUsesFromSteps(
|
|
21
|
+
export declare function extractUsesFromSteps(options: ExtractUsesOptions): GitHubAction[];
|
|
22
|
+
export {};
|
|
@@ -5,18 +5,19 @@ import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
|
5
5
|
import { isScalar } from "../guards/is-scalar.js";
|
|
6
6
|
import { isNode } from "../guards/is-node.js";
|
|
7
7
|
import { isPair } from "../guards/is-pair.js";
|
|
8
|
-
function extractUsesFromSteps(s
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
function extractUsesFromSteps(s) {
|
|
9
|
+
let { stepsNode: c, filePath: l, content: u, jobName: d } = s;
|
|
10
|
+
if (!isYAMLSequence(c)) return [];
|
|
11
|
+
let f = [];
|
|
12
|
+
for (let o of c.items) {
|
|
12
13
|
if (!isYAMLMap(o) || !isNode(o)) continue;
|
|
13
14
|
let s = o.toJSON();
|
|
14
15
|
if (typeof s != "object" || !s || Array.isArray(s)) continue;
|
|
15
|
-
let
|
|
16
|
-
if (typeof
|
|
17
|
-
let
|
|
18
|
-
|
|
16
|
+
let c = s;
|
|
17
|
+
if (typeof c.uses != "string") continue;
|
|
18
|
+
let p = o.items.find((e) => isPair(e) && isScalar(e.key) && e.key.value === "uses"), m = p?.key ? getLineNumberForKey(u, p.key) : 0, h = parseActionReference(c.uses, l, m);
|
|
19
|
+
h && (d && (h.job = d), f.push(h));
|
|
19
20
|
}
|
|
20
|
-
return
|
|
21
|
+
return f;
|
|
21
22
|
}
|
|
22
23
|
export { extractUsesFromSteps };
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { ActionUpdate } from '../../types/action-update';
|
|
2
|
-
|
|
2
|
+
interface PromptUpdateSelectionOptions {
|
|
3
|
+
/** Whether to show the Age column. */
|
|
4
|
+
showAge?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function promptUpdateSelection(updates: ActionUpdate[], options?: PromptUpdateSelectionOptions): Promise<ActionUpdate[] | null>;
|
|
7
|
+
export {};
|
|
@@ -7,97 +7,118 @@ import pc from "picocolors";
|
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import enquirer from "enquirer";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
var MIN_ACTION_WIDTH =
|
|
11
|
-
async function promptUpdateSelection(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
s
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
var MIN_ACTION_WIDTH = 40, MIN_JOB_WIDTH = 4, MIN_CURRENT_WIDTH = 16, MAX_VERSION_WIDTH = 7;
|
|
11
|
+
async function promptUpdateSelection(l, g = {}) {
|
|
12
|
+
let { showAge: y = !1 } = g;
|
|
13
|
+
if (l.length === 0) return null;
|
|
14
|
+
let b = l.filter((e) => e.hasUpdate);
|
|
15
|
+
if (b.length === 0) return console.info(pc.green("✓ All actions are up to date!")), null;
|
|
16
|
+
let x = /* @__PURE__ */ new Map();
|
|
17
|
+
for (let [e, o] of b.entries()) {
|
|
18
|
+
let s = o.action.file ?? "unknown file", c = path.relative(path.join(process.cwd(), GITHUB_DIRECTORY), s);
|
|
19
|
+
c === "" && (c = s);
|
|
20
|
+
let l = x.get(c) ?? [];
|
|
21
|
+
l.push({
|
|
22
|
+
update: o,
|
|
22
23
|
index: e
|
|
23
|
-
}),
|
|
24
|
+
}), x.set(c, l);
|
|
24
25
|
}
|
|
25
|
-
let
|
|
26
|
-
let
|
|
26
|
+
let S = await Promise.all(b.map(async (e) => {
|
|
27
|
+
let a = formatVersionOrSha(e.currentVersion), o = e.currentVersion ?? void 0, s = null, c = null;
|
|
27
28
|
if (!e.currentVersion || !isSha(e.currentVersion)) return {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
versionForPadding: s,
|
|
30
|
+
effectiveForDiff: o,
|
|
31
|
+
shortSha: c,
|
|
32
|
+
display: a
|
|
30
33
|
};
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
effectiveForDiff: a,
|
|
38
|
-
display: i
|
|
34
|
+
let l = await tryReadInlineVersionComment(e.action.file, e.action.line);
|
|
35
|
+
return l && (c = e.currentVersion.slice(0, 7), s = formatVersionOrSha(l), a = s, o = l), {
|
|
36
|
+
versionForPadding: s,
|
|
37
|
+
effectiveForDiff: o,
|
|
38
|
+
shortSha: c,
|
|
39
|
+
display: a
|
|
39
40
|
};
|
|
40
|
-
})),
|
|
41
|
-
for (let [
|
|
42
|
-
let
|
|
43
|
-
|
|
41
|
+
})), C = [], w = stripAnsi("Action").length, T = stripAnsi("Current").length, E = stripAnsi("Job").length, D = 0, O = !1;
|
|
42
|
+
for (let [a, l] of b.entries()) {
|
|
43
|
+
let u = l.action.name, d = S[a], f = d.display, p = l.action.job ?? "–";
|
|
44
|
+
if (w = Math.max(w, u.length), T = Math.max(T, stripAnsi(f).length, d.versionForPadding && d.shortSha ? stripAnsi(`${padString(d.versionForPadding, D + 1)}${pc.gray(`(${d.shortSha})`)}`).length : 0), E = Math.max(E, p.length), l.latestVersion) {
|
|
45
|
+
let s = formatVersion(l.latestVersion, S[a]?.effectiveForDiff ?? l.currentVersion);
|
|
46
|
+
D = Math.max(D, stripAnsi(s).length);
|
|
47
|
+
}
|
|
48
|
+
let m = S[a]?.versionForPadding;
|
|
49
|
+
m && (D = Math.max(D, stripAnsi(m).length)), l.publishedAt && (O = !0);
|
|
44
50
|
}
|
|
45
|
-
let
|
|
46
|
-
for (let [
|
|
47
|
-
let
|
|
48
|
-
if (!
|
|
49
|
-
console.warn(`Unexpected missing group for file: ${
|
|
51
|
+
let k = Math.max(w, MIN_ACTION_WIDTH), A = Math.max(T, MIN_CURRENT_WIDTH), j = Math.max(E, MIN_JOB_WIDTH), M = Math.min(D, MAX_VERSION_WIDTH), N = M + 1 + 9, P = y && O ? 6 : 0, F = [...x.keys()].toSorted();
|
|
52
|
+
for (let [a, o] of F.entries()) {
|
|
53
|
+
let l = x.get(o);
|
|
54
|
+
if (!l) {
|
|
55
|
+
console.warn(`Unexpected missing group for file: ${o}`);
|
|
50
56
|
continue;
|
|
51
57
|
}
|
|
52
|
-
let
|
|
53
|
-
|
|
58
|
+
let u = [], d = l;
|
|
59
|
+
u.push({
|
|
54
60
|
current: "Current",
|
|
55
61
|
action: "Action",
|
|
56
62
|
target: "Target",
|
|
57
|
-
arrow: "❯"
|
|
63
|
+
arrow: "❯",
|
|
64
|
+
job: "Job",
|
|
65
|
+
age: "Age"
|
|
58
66
|
});
|
|
59
|
-
for (let { update:
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
for (let { update: a, index: o } of d) {
|
|
68
|
+
let l = !!a.latestSha, d = S[o], f = d.display;
|
|
69
|
+
d.versionForPadding && d.shortSha && (f = `${padString(d.versionForPadding, M + 1)}${pc.gray(`(${d.shortSha})`)}`);
|
|
70
|
+
let p = d.effectiveForDiff ?? a.currentVersion, m = formatVersion(a.latestVersion, p), h = a.action.name;
|
|
71
|
+
if (a.latestSha) {
|
|
72
|
+
let e = a.latestSha.slice(0, 7);
|
|
73
|
+
m = `${padString(m, M + 1)}${pc.gray(`(${e})`)}`;
|
|
64
74
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
l || (m = pc.gray(m), f = pc.gray(f), h = pc.gray(h));
|
|
76
|
+
let g = a.action.job ?? "–", _ = formatAge(a.publishedAt);
|
|
77
|
+
u.push({
|
|
78
|
+
job: l ? g : pc.gray(g),
|
|
79
|
+
age: l ? _ : pc.gray(_),
|
|
80
|
+
action: h,
|
|
81
|
+
target: m,
|
|
68
82
|
arrow: "❯",
|
|
69
|
-
current:
|
|
83
|
+
current: f
|
|
70
84
|
});
|
|
71
85
|
}
|
|
72
|
-
let
|
|
73
|
-
for (let [e,
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
let h = Math.max(k, MIN_ACTION_WIDTH), g = Math.max(A, MIN_CURRENT_WIDTH), _ = Math.max(j, MIN_JOB_WIDTH), v = [];
|
|
87
|
+
for (let [e, a] of u.entries()) {
|
|
88
|
+
let o = e === 0, s = formatTableRow({
|
|
89
|
+
targetWidth: N,
|
|
90
|
+
currentWidth: g,
|
|
91
|
+
actionWidth: h,
|
|
92
|
+
ageWidth: P,
|
|
93
|
+
jobWidth: _,
|
|
94
|
+
row: a
|
|
95
|
+
});
|
|
96
|
+
if (o) v.push({
|
|
97
|
+
message: pc.gray(` ○ ${s}`),
|
|
77
98
|
role: "separator",
|
|
78
99
|
indent: "",
|
|
79
100
|
name: ""
|
|
80
101
|
});
|
|
81
102
|
else {
|
|
82
|
-
let { update:
|
|
83
|
-
|
|
84
|
-
message:
|
|
85
|
-
value: String(
|
|
86
|
-
name: String(
|
|
87
|
-
disabled: !
|
|
103
|
+
let { update: a, index: o } = d[e - 1], c = !!a.latestSha, l = c && !a.isBreaking;
|
|
104
|
+
v.push({
|
|
105
|
+
message: s,
|
|
106
|
+
value: String(o),
|
|
107
|
+
name: String(o),
|
|
108
|
+
disabled: !c,
|
|
88
109
|
indent: "",
|
|
89
|
-
enabled:
|
|
110
|
+
enabled: l
|
|
90
111
|
});
|
|
91
112
|
}
|
|
92
113
|
}
|
|
93
|
-
|
|
94
|
-
message: pc.gray(
|
|
95
|
-
value: `label|${
|
|
96
|
-
choices:
|
|
97
|
-
name: `label|${
|
|
114
|
+
C.push({
|
|
115
|
+
message: pc.gray(o),
|
|
116
|
+
value: `label|${o}`,
|
|
117
|
+
choices: v,
|
|
118
|
+
name: `label|${o}`,
|
|
98
119
|
isGroupLabel: !0,
|
|
99
120
|
enabled: !1
|
|
100
|
-
}),
|
|
121
|
+
}), a < F.length - 1 && C.push({
|
|
101
122
|
role: "separator",
|
|
102
123
|
message: " ",
|
|
103
124
|
name: ""
|
|
@@ -105,12 +126,12 @@ async function promptUpdateSelection(o) {
|
|
|
105
126
|
}
|
|
106
127
|
try {
|
|
107
128
|
let e = {
|
|
108
|
-
indicator(e,
|
|
109
|
-
if (
|
|
110
|
-
let e = (
|
|
111
|
-
return ` ${pc.gray(
|
|
129
|
+
indicator(e, a) {
|
|
130
|
+
if (a.isGroupLabel) {
|
|
131
|
+
let e = (a.choices ?? []).filter((e) => !("role" in e)), o = e.length, s = e.filter((e) => !!e.enabled).length === o ? "●" : "○";
|
|
132
|
+
return ` ${pc.gray(s)}`;
|
|
112
133
|
}
|
|
113
|
-
return ` ${
|
|
134
|
+
return ` ${a.enabled ? "●" : "○"}`;
|
|
114
135
|
},
|
|
115
136
|
message: `Choose which actions to update (Press ${pc.cyan("<space>")} to select, ${pc.cyan("<a>")} to toggle all, ${pc.cyan("<i>")} to invert selection)`,
|
|
116
137
|
styles: {
|
|
@@ -131,46 +152,53 @@ async function promptUpdateSelection(o) {
|
|
|
131
152
|
type: "multiselect",
|
|
132
153
|
name: "selected",
|
|
133
154
|
pointer: "❯",
|
|
134
|
-
choices:
|
|
135
|
-
}, { selected:
|
|
136
|
-
for (let e of
|
|
155
|
+
choices: C
|
|
156
|
+
}, { selected: a } = await enquirer.prompt(e), o = /* @__PURE__ */ new Set();
|
|
157
|
+
for (let e of a) {
|
|
137
158
|
if (e.startsWith("label|")) {
|
|
138
|
-
let
|
|
139
|
-
for (let { update: e, index:
|
|
159
|
+
let a = e.slice(6), s = x.get(a) ?? [];
|
|
160
|
+
for (let { update: e, index: a } of s) e.latestSha && o.add(a);
|
|
140
161
|
continue;
|
|
141
162
|
}
|
|
142
|
-
let
|
|
143
|
-
Number.isFinite(
|
|
163
|
+
let a = Number.parseInt(e, 10);
|
|
164
|
+
Number.isFinite(a) && o.add(a);
|
|
144
165
|
}
|
|
145
|
-
let
|
|
146
|
-
for (let [e,
|
|
147
|
-
return
|
|
166
|
+
let s = [];
|
|
167
|
+
for (let [e, a] of b.entries()) o.has(e) && a.latestSha && s.push(a);
|
|
168
|
+
return s.length === 0 ? (console.info(pc.yellow("\nNo actions selected")), null) : s;
|
|
148
169
|
} catch (e) {
|
|
149
170
|
if (e instanceof Error && (e.message.includes("cancelled") || e.message.includes("ESC") || e.name === "ExitPromptError")) return logSelectionCancelled(), null;
|
|
150
171
|
throw console.error(pc.red("Unexpected error during selection:"), e), e;
|
|
151
172
|
}
|
|
152
173
|
}
|
|
153
|
-
async function tryReadInlineVersionComment(e,
|
|
174
|
+
async function tryReadInlineVersionComment(e, a) {
|
|
154
175
|
try {
|
|
155
|
-
if (!e || !
|
|
156
|
-
let
|
|
157
|
-
if (
|
|
158
|
-
let
|
|
159
|
-
if (
|
|
176
|
+
if (!e || !a || a <= 0) return null;
|
|
177
|
+
let o = (await readFile(e, "utf8")).split("\n"), s = a - 1;
|
|
178
|
+
if (s < 0 || s >= o.length) return null;
|
|
179
|
+
let c = o[s].match(/#\s*(?<version>[Vv]?\d+(?:\.\d+){0,2}(?:[+-][\w\-.]+)?)/u);
|
|
180
|
+
if (c?.groups?.version) return c.groups.version;
|
|
160
181
|
} catch {}
|
|
161
182
|
return null;
|
|
162
183
|
}
|
|
163
|
-
function
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
function formatAge(e) {
|
|
185
|
+
if (!e) return "";
|
|
186
|
+
let a = Date.now() - e.getTime(), o = Math.floor(a / (1e3 * 60 * 60)), s = Math.floor(o / 24), c = Math.floor(s / 7), l = s % 7;
|
|
187
|
+
return c >= 1 ? l > 0 ? `${c}w ${l}d` : `${c}w` : s >= 1 ? `${s}d` : `${o}h`;
|
|
188
|
+
}
|
|
189
|
+
function formatTableRow(e) {
|
|
190
|
+
let { currentWidth: a, actionWidth: o, targetWidth: c, jobWidth: l, ageWidth: u, row: d } = e, f = [
|
|
191
|
+
padString(d.action, o),
|
|
192
|
+
padString(d.job, l),
|
|
193
|
+
padString(d.current, a),
|
|
194
|
+
d.arrow,
|
|
195
|
+
padString(d.target, c)
|
|
196
|
+
];
|
|
197
|
+
return u > 0 && f.push(d.age), f.join(" ").replace(/\s+$/u, "");
|
|
170
198
|
}
|
|
171
199
|
function isSha(e) {
|
|
172
|
-
let
|
|
173
|
-
return /^[0-9a-f]{7,40}$/iu.test(
|
|
200
|
+
let a = e.replace(/^v/u, "");
|
|
201
|
+
return /^[0-9a-f]{7,40}$/iu.test(a);
|
|
174
202
|
}
|
|
175
203
|
function formatVersionOrSha(e) {
|
|
176
204
|
return e ? isSha(e) ? e.slice(0, 7) : e.replace(/^v/u, "") : pc.gray("unknown");
|
|
@@ -24,7 +24,7 @@ function parseActionReference(e, t, n) {
|
|
|
24
24
|
if (!s || !c) return null;
|
|
25
25
|
for (let e of o.slice(2)) if (!e) return null;
|
|
26
26
|
return {
|
|
27
|
-
type: "external",
|
|
27
|
+
type: o.length > 2 && (i.endsWith(".yml") || i.endsWith(".yaml")) ? "reusable-workflow" : "external",
|
|
28
28
|
name: i,
|
|
29
29
|
version: a,
|
|
30
30
|
file: t,
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "1.
|
|
1
|
+
const version = "1.7.0";
|
|
2
2
|
export { version };
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { GitHubAction } from './github-action';
|
|
2
2
|
/** Update information for a GitHub Action. */
|
|
3
3
|
export interface ActionUpdate {
|
|
4
|
+
/** Reason for skipping the update check. */
|
|
5
|
+
skipReason?: 'unknown' | 'branch'
|
|
6
|
+
|
|
4
7
|
/** Current version string. */
|
|
5
8
|
currentVersion: string | null
|
|
6
9
|
|
|
7
10
|
/** Latest available version. */
|
|
8
11
|
latestVersion: string | null
|
|
9
12
|
|
|
13
|
+
/** Status of the check for this action. */
|
|
14
|
+
status?: 'skipped' | 'ok'
|
|
15
|
+
|
|
10
16
|
/** SHA hash of the latest version. */
|
|
11
17
|
latestSha: string | null
|
|
12
18
|
|
|
19
|
+
/** Publication date of the latest version (null if unknown). */
|
|
20
|
+
publishedAt: Date | null
|
|
21
|
+
|
|
13
22
|
/** The original action from scanning. */
|
|
14
23
|
action: GitHubAction
|
|
15
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Represents a GitHub Action used in workflows or composite actions. */
|
|
2
2
|
export interface GitHubAction {
|
|
3
3
|
/** Type of the GitHub Action. */
|
|
4
|
-
type: 'composite' | 'external' | 'docker' | 'local'
|
|
4
|
+
type: 'reusable-workflow' | 'composite' | 'external' | 'docker' | 'local'
|
|
5
5
|
|
|
6
6
|
/** Version or tag of the action (e.g., 'v1', 'main', commit SHA). */
|
|
7
7
|
version?: string | null
|
|
@@ -15,6 +15,9 @@ export interface GitHubAction {
|
|
|
15
15
|
/** Original `uses` string from workflow, if available. */
|
|
16
16
|
uses?: string
|
|
17
17
|
|
|
18
|
+
/** Name of the job where this action is used (for workflows). */
|
|
19
|
+
job?: string
|
|
20
|
+
|
|
18
21
|
/** Full name of the action (e.g., 'actions/checkout'). */
|
|
19
22
|
name: string
|
|
20
23
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { WorkflowStep } from './workflow-step';
|
|
2
2
|
/** Represents a job in a GitHub Actions workflow. */
|
|
3
3
|
export interface WorkflowJob {
|
|
4
|
+
/** Secrets passed to the reusable workflow ('inherit' or specific secrets). */
|
|
5
|
+
secrets?: Record<string, unknown> | 'inherit'
|
|
6
|
+
|
|
7
|
+
/** Input parameters passed to the reusable workflow. */
|
|
8
|
+
with?: Record<string, unknown>
|
|
9
|
+
|
|
4
10
|
/** Runner environment(s) to execute this job on (e.g., 'ubuntu-latest'). */
|
|
5
11
|
'runs-on'?: string[] | string
|
|
6
12
|
|
|
@@ -13,6 +19,9 @@ export interface WorkflowJob {
|
|
|
13
19
|
/** Allow additional properties for job configuration. */
|
|
14
20
|
[key: string]: unknown
|
|
15
21
|
|
|
22
|
+
/** Reusable workflow reference (mutually exclusive with 'steps'). */
|
|
23
|
+
uses?: string
|
|
24
|
+
|
|
16
25
|
/** Conditional expression to determine if the job should run. */
|
|
17
26
|
if?: string
|
|
18
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actions-up",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"nanospinner": "^1.2.2",
|
|
42
42
|
"picocolors": "^1.1.1",
|
|
43
43
|
"semver": "^7.7.3",
|
|
44
|
-
"yaml": "^2.8.
|
|
44
|
+
"yaml": "^2.8.2"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": "^18.0.0 || >=20.0.0"
|
package/readme.md
CHANGED
|
@@ -19,6 +19,7 @@ Interactively upgrade and pin actions to exact commit SHAs for secure, reproduci
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
21
|
- **Auto-discovery**: Scans all workflows (`.github/workflows/*.yml`) and composite actions (`.github/actions/*/action.yml`)
|
|
22
|
+
- **Reusable Workflows**: Detects and updates reusable workflow calls at the job level
|
|
22
23
|
- **SHA pinning**: Updates actions to use commit SHA instead of tags for better security
|
|
23
24
|
- **Batch Updates**: Update multiple actions at once
|
|
24
25
|
- **Interactive Selection**: Choose which actions to update
|
|
@@ -130,6 +131,10 @@ By default, Actions Up scans the `.github` directory. You can specify a differen
|
|
|
130
131
|
npx actions-up --dir .gitea
|
|
131
132
|
```
|
|
132
133
|
|
|
134
|
+
### Branch References
|
|
135
|
+
|
|
136
|
+
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`.
|
|
137
|
+
|
|
133
138
|
## GitHub Actions Integration
|
|
134
139
|
|
|
135
140
|
### Automated PR Checks
|
|
@@ -379,6 +384,8 @@ jobs:
|
|
|
379
384
|
|
|
380
385
|
## Example
|
|
381
386
|
|
|
387
|
+
### Regular Actions
|
|
388
|
+
|
|
382
389
|
```yaml
|
|
383
390
|
# Before
|
|
384
391
|
- uses: actions/checkout@v3
|
|
@@ -389,6 +396,26 @@ jobs:
|
|
|
389
396
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
390
397
|
```
|
|
391
398
|
|
|
399
|
+
### Reusable Workflows
|
|
400
|
+
|
|
401
|
+
Actions Up also detects and updates reusable workflow calls:
|
|
402
|
+
|
|
403
|
+
```yaml
|
|
404
|
+
# Before
|
|
405
|
+
jobs:
|
|
406
|
+
call-workflow:
|
|
407
|
+
uses: org/repo/.github/workflows/ci.yml@v1.0.0
|
|
408
|
+
with:
|
|
409
|
+
config: production
|
|
410
|
+
|
|
411
|
+
# After running actions-up
|
|
412
|
+
jobs:
|
|
413
|
+
call-workflow:
|
|
414
|
+
uses: org/repo/.github/workflows/ci.yml@a1b2c3d4e5f6 # v2.0.0
|
|
415
|
+
with:
|
|
416
|
+
config: production
|
|
417
|
+
```
|
|
418
|
+
|
|
392
419
|
## Advanced Usage
|
|
393
420
|
|
|
394
421
|
### Using GitHub Token for Higher Rate Limits
|
|
@@ -439,6 +466,17 @@ npx actions-up --exclude ".*/internal-.*" --exclude "/^acme\/.+$/i"
|
|
|
439
466
|
npx actions-up --exclude "my-org/.*, .*/internal-.*"
|
|
440
467
|
```
|
|
441
468
|
|
|
469
|
+
#### Filtering by Release Age
|
|
470
|
+
|
|
471
|
+
By default, Actions Up shows all available updates. You can filter out recently released updates to avoid updating to versions that haven't been battle-tested yet:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# Only show updates released at least 7 days ago
|
|
475
|
+
npx actions-up --min-age 7
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
When `--min-age` is set, an "Age" column appears showing how long ago each release was published (e.g., `3d`, `1w 2d`).
|
|
479
|
+
|
|
442
480
|
#### Ignore Comments
|
|
443
481
|
|
|
444
482
|
You can skip specific actions or files using YAML comments. Ignored items are hidden in dry-run and interactive modes and are not updated with `--yes`.
|