actions-up 1.9.0 â 1.10.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 +73 -39
- package/dist/core/interactive/prompt-update-selection.js +107 -120
- package/dist/core/versions/get-update-level.d.ts +11 -0
- package/dist/core/versions/get-update-level.js +23 -0
- package/dist/core/versions/is-sha.d.ts +7 -0
- package/dist/core/versions/is-sha.js +6 -0
- package/dist/core/versions/read-inline-version-comment.d.ts +13 -0
- package/dist/core/versions/read-inline-version-comment.js +14 -0
- package/dist/package.js +1 -1
- package/dist/types/update-mode.d.ts +2 -0
- package/package.json +2 -2
- package/readme.md +43 -102
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { readInlineVersionComment } from "../core/versions/read-inline-version-comment.js";
|
|
2
|
+
import { isSha } from "../core/versions/is-sha.js";
|
|
1
3
|
import { promptUpdateSelection } from "../core/interactive/prompt-update-selection.js";
|
|
4
|
+
import { getUpdateLevel } from "../core/versions/get-update-level.js";
|
|
2
5
|
import { applyUpdates } from "../core/ast/update/apply-updates.js";
|
|
3
6
|
import { shouldIgnore } from "../core/ignore/should-ignore.js";
|
|
4
7
|
import { checkUpdates } from "../core/api/check-updates.js";
|
|
@@ -10,75 +13,106 @@ import "node:worker_threads";
|
|
|
10
13
|
import pc from "picocolors";
|
|
11
14
|
import cac from "cac";
|
|
12
15
|
function run() {
|
|
13
|
-
let
|
|
14
|
-
|
|
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) => {
|
|
15
18
|
console.info(pc.cyan("\nđ Actions Up!\n"));
|
|
16
|
-
let
|
|
19
|
+
let m = createSpinner("Scanning GitHub Actions...").start();
|
|
17
20
|
try {
|
|
18
|
-
let
|
|
19
|
-
if (
|
|
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) {
|
|
20
23
|
console.info(pc.green("\n⨠No GitHub Actions found in this repository"));
|
|
21
24
|
return;
|
|
22
25
|
}
|
|
23
|
-
let
|
|
24
|
-
Array.isArray(
|
|
25
|
-
let
|
|
26
|
-
if (
|
|
27
|
-
let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"),
|
|
28
|
-
|
|
29
|
-
let { name:
|
|
30
|
-
for (let e of
|
|
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;
|
|
31
34
|
return !0;
|
|
32
35
|
}));
|
|
33
36
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
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"));
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
|
-
let
|
|
39
|
-
await Promise.all(
|
|
40
|
-
await shouldIgnore(e.action.file, e.action.line) ||
|
|
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);
|
|
41
44
|
}));
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
if (
|
|
46
|
-
|
|
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);
|
|
54
|
+
}
|
|
55
|
+
let f = getUpdateLevel(d, u.latestVersion);
|
|
56
|
+
return {
|
|
57
|
+
allowed: k === "minor" ? f === "minor" || f === "patch" || f === "none" : f === "patch" || f === "none",
|
|
58
|
+
update: u
|
|
59
|
+
};
|
|
60
|
+
})), d = [];
|
|
61
|
+
for (let e of u) e.allowed ? d.push(e.update) : A.push(e.update);
|
|
62
|
+
E = d;
|
|
63
|
+
}
|
|
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"));
|
|
47
67
|
return;
|
|
48
68
|
}
|
|
49
|
-
if (
|
|
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) {
|
|
50
70
|
console.info(pc.yellow("\nđ Dry Run - No changes will be made\n"));
|
|
51
|
-
for (let e of
|
|
52
|
-
console.info(pc.gray(`\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`));
|
|
53
73
|
return;
|
|
54
74
|
}
|
|
55
|
-
if (
|
|
56
|
-
let e =
|
|
75
|
+
if (p.yes) {
|
|
76
|
+
let e = E.filter((e) => e.latestSha);
|
|
57
77
|
if (e.length === 0) {
|
|
58
78
|
console.info(pc.yellow("\nâ ď¸ No actions with SHA available for update\n"));
|
|
59
79
|
return;
|
|
60
80
|
}
|
|
61
81
|
console.info(pc.yellow(`\nđ Updating ${e.length} actions...\n`)), await applyUpdates(e), console.info(pc.green("\nâ Updates applied successfully!"));
|
|
62
82
|
} else {
|
|
63
|
-
|
|
64
|
-
|
|
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) {
|
|
65
86
|
console.info(pc.gray("\nNo updates applied"));
|
|
66
87
|
return;
|
|
67
88
|
}
|
|
68
|
-
console.info(pc.yellow(`\nđ Updating ${
|
|
89
|
+
console.info(pc.yellow(`\nđ Updating ${e.length} selected actions...\n`)), await applyUpdates(e), console.info(pc.green("\nâ Updates applied successfully!"));
|
|
69
90
|
}
|
|
70
91
|
} catch (e) {
|
|
71
|
-
|
|
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);
|
|
72
93
|
}
|
|
73
|
-
}),
|
|
94
|
+
}), h.parse();
|
|
74
95
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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"}`;
|
|
80
102
|
console.info(pc.gray(` ⢠${e}`));
|
|
81
103
|
}
|
|
82
|
-
|
|
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".`);
|
|
83
117
|
}
|
|
84
118
|
export { run };
|
|
@@ -1,62 +1,63 @@
|
|
|
1
|
+
import { readInlineVersionComment } from "../versions/read-inline-version-comment.js";
|
|
1
2
|
import { formatVersion } from "./format-version.js";
|
|
2
3
|
import { GITHUB_DIRECTORY } from "../constants.js";
|
|
4
|
+
import { isSha } from "../versions/is-sha.js";
|
|
3
5
|
import { stripAnsi } from "./strip-ansi.js";
|
|
4
6
|
import { padString } from "./pad-string.js";
|
|
5
7
|
import "node:worker_threads";
|
|
6
8
|
import pc from "picocolors";
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
9
|
import enquirer from "enquirer";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
var MIN_ACTION_WIDTH = 40, MIN_JOB_WIDTH = 4, MIN_CURRENT_WIDTH = 16, MAX_VERSION_WIDTH = 7;
|
|
11
|
-
async function promptUpdateSelection(
|
|
12
|
-
let { showAge: y = !1 } =
|
|
13
|
-
if (
|
|
14
|
-
let b =
|
|
12
|
+
async function promptUpdateSelection(g, v = {}) {
|
|
13
|
+
let { showAge: y = !1 } = v;
|
|
14
|
+
if (g.length === 0) return null;
|
|
15
|
+
let b = g.filter((t) => t.hasUpdate);
|
|
15
16
|
if (b.length === 0) return console.info(pc.green("â All actions are up to date!")), null;
|
|
16
17
|
let x = /* @__PURE__ */ new Map();
|
|
17
|
-
for (let [
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
update:
|
|
23
|
-
index:
|
|
24
|
-
}), x.set(
|
|
18
|
+
for (let [t, i] of b.entries()) {
|
|
19
|
+
let o = i.action.file ?? "unknown file", s = path.relative(path.join(process.cwd(), GITHUB_DIRECTORY), o);
|
|
20
|
+
s === "" && (s = o);
|
|
21
|
+
let c = x.get(s) ?? [];
|
|
22
|
+
c.push({
|
|
23
|
+
update: i,
|
|
24
|
+
index: t
|
|
25
|
+
}), x.set(s, c);
|
|
25
26
|
}
|
|
26
|
-
let S = await Promise.all(b.map(async (
|
|
27
|
-
let a = formatVersionOrSha(
|
|
28
|
-
if (!
|
|
29
|
-
versionForPadding:
|
|
30
|
-
effectiveForDiff:
|
|
31
|
-
shortSha:
|
|
27
|
+
let S = await Promise.all(b.map(async (i) => {
|
|
28
|
+
let a = formatVersionOrSha(i.currentVersion), s = i.currentVersion ?? void 0, c = null, l = null;
|
|
29
|
+
if (!i.currentVersion || !isSha(i.currentVersion)) return {
|
|
30
|
+
versionForPadding: c,
|
|
31
|
+
effectiveForDiff: s,
|
|
32
|
+
shortSha: l,
|
|
32
33
|
display: a
|
|
33
34
|
};
|
|
34
|
-
let
|
|
35
|
-
return
|
|
36
|
-
versionForPadding:
|
|
37
|
-
effectiveForDiff:
|
|
38
|
-
shortSha:
|
|
35
|
+
let u = await readInlineVersionComment(i.action.file, i.action.line);
|
|
36
|
+
return u && (l = i.currentVersion.slice(0, 7), c = formatVersionOrSha(u), a = c, s = u), {
|
|
37
|
+
versionForPadding: c,
|
|
38
|
+
effectiveForDiff: s,
|
|
39
|
+
shortSha: l,
|
|
39
40
|
display: a
|
|
40
41
|
};
|
|
41
42
|
})), C = [], w = stripAnsi("Action").length, T = stripAnsi("Current").length, E = stripAnsi("Job").length, D = 0, O = !1;
|
|
42
|
-
for (let [
|
|
43
|
-
let
|
|
44
|
-
if (w = Math.max(w,
|
|
45
|
-
let
|
|
46
|
-
D = Math.max(D, stripAnsi(
|
|
43
|
+
for (let [t, a] of b.entries()) {
|
|
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
|
+
let o = formatVersion(a.latestVersion, S[t]?.effectiveForDiff ?? a.currentVersion);
|
|
47
|
+
D = Math.max(D, stripAnsi(o).length);
|
|
47
48
|
}
|
|
48
|
-
let
|
|
49
|
-
|
|
49
|
+
let p = S[t]?.versionForPadding;
|
|
50
|
+
p && (D = Math.max(D, stripAnsi(p).length)), a.publishedAt && (O = !0);
|
|
50
51
|
}
|
|
51
52
|
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 [
|
|
53
|
-
let
|
|
54
|
-
if (!
|
|
55
|
-
console.warn(`Unexpected missing group for file: ${
|
|
53
|
+
for (let [t, a] of F.entries()) {
|
|
54
|
+
let o = x.get(a);
|
|
55
|
+
if (!o) {
|
|
56
|
+
console.warn(`Unexpected missing group for file: ${a}`);
|
|
56
57
|
continue;
|
|
57
58
|
}
|
|
58
|
-
let
|
|
59
|
-
|
|
59
|
+
let s = [], u = o;
|
|
60
|
+
s.push({
|
|
60
61
|
current: "Current",
|
|
61
62
|
action: "Action",
|
|
62
63
|
target: "Target",
|
|
@@ -64,74 +65,74 @@ async function promptUpdateSelection(l, g = {}) {
|
|
|
64
65
|
job: "Job",
|
|
65
66
|
age: "Age"
|
|
66
67
|
});
|
|
67
|
-
for (let { update:
|
|
68
|
-
let
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
if (
|
|
72
|
-
let
|
|
73
|
-
|
|
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
|
+
if (t.latestSha) {
|
|
73
|
+
let i = t.latestSha.slice(0, 7);
|
|
74
|
+
p = `${padString(p, M + 1)}${pc.gray(`(${i})`)}`;
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
job:
|
|
79
|
-
age:
|
|
80
|
-
action:
|
|
81
|
-
target:
|
|
76
|
+
o || (p = pc.gray(p), d = pc.gray(d), m = pc.gray(m));
|
|
77
|
+
let h = t.action.job ?? "â", g = formatAge(t.publishedAt);
|
|
78
|
+
s.push({
|
|
79
|
+
job: o ? h : pc.gray(h),
|
|
80
|
+
age: o ? g : pc.gray(g),
|
|
81
|
+
action: m,
|
|
82
|
+
target: p,
|
|
82
83
|
arrow: "âŻ",
|
|
83
|
-
current:
|
|
84
|
+
current: d
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
|
-
let
|
|
87
|
-
for (let [
|
|
88
|
-
let
|
|
87
|
+
let d = Math.max(k, MIN_ACTION_WIDTH), h = Math.max(A, MIN_CURRENT_WIDTH), g = Math.max(j, MIN_JOB_WIDTH), _ = [];
|
|
88
|
+
for (let [t, i] of s.entries()) {
|
|
89
|
+
let a = t === 0, o = formatTableRow({
|
|
89
90
|
targetWidth: N,
|
|
90
|
-
currentWidth:
|
|
91
|
-
actionWidth:
|
|
91
|
+
currentWidth: h,
|
|
92
|
+
actionWidth: d,
|
|
92
93
|
ageWidth: P,
|
|
93
|
-
jobWidth:
|
|
94
|
-
row:
|
|
94
|
+
jobWidth: g,
|
|
95
|
+
row: i
|
|
95
96
|
});
|
|
96
|
-
if (
|
|
97
|
-
message: pc.gray(` â ${
|
|
97
|
+
if (a) _.push({
|
|
98
|
+
message: pc.gray(` â ${o}`),
|
|
98
99
|
role: "separator",
|
|
99
100
|
indent: "",
|
|
100
101
|
name: ""
|
|
101
102
|
});
|
|
102
103
|
else {
|
|
103
|
-
let { update:
|
|
104
|
-
|
|
105
|
-
message:
|
|
106
|
-
value: String(
|
|
107
|
-
name: String(
|
|
108
|
-
disabled: !
|
|
104
|
+
let { update: i, index: a } = u[t - 1], s = !!i.latestSha, c = s && !i.isBreaking;
|
|
105
|
+
_.push({
|
|
106
|
+
message: o,
|
|
107
|
+
value: String(a),
|
|
108
|
+
name: String(a),
|
|
109
|
+
disabled: !s,
|
|
109
110
|
indent: "",
|
|
110
|
-
enabled:
|
|
111
|
+
enabled: c
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
C.push({
|
|
115
|
-
message: pc.gray(
|
|
116
|
-
value: `label|${
|
|
117
|
-
choices:
|
|
118
|
-
name: `label|${
|
|
116
|
+
message: pc.gray(a),
|
|
117
|
+
value: `label|${a}`,
|
|
118
|
+
choices: _,
|
|
119
|
+
name: `label|${a}`,
|
|
119
120
|
isGroupLabel: !0,
|
|
120
121
|
enabled: !1
|
|
121
|
-
}),
|
|
122
|
+
}), t < F.length - 1 && C.push({
|
|
122
123
|
role: "separator",
|
|
123
124
|
message: " ",
|
|
124
125
|
name: ""
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
128
|
try {
|
|
128
|
-
let
|
|
129
|
-
indicator(
|
|
130
|
-
if (
|
|
131
|
-
let
|
|
132
|
-
return ` ${pc.gray(
|
|
129
|
+
let t = {
|
|
130
|
+
indicator(t, i) {
|
|
131
|
+
if (i.isGroupLabel) {
|
|
132
|
+
let t = (i.choices ?? []).filter((t) => !("role" in t)), a = t.length, o = t.filter((t) => !!t.enabled).length === a ? "â" : "â";
|
|
133
|
+
return ` ${pc.gray(o)}`;
|
|
133
134
|
}
|
|
134
|
-
return ` ${
|
|
135
|
+
return ` ${i.enabled ? "â" : "â"}`;
|
|
135
136
|
},
|
|
136
137
|
message: `Choose which actions to update (Press ${pc.cyan("<space>")} to select, ${pc.cyan("<a>")} to toggle all, ${pc.cyan("<i>")} to invert selection)`,
|
|
137
138
|
styles: {
|
|
@@ -153,55 +154,41 @@ async function promptUpdateSelection(l, g = {}) {
|
|
|
153
154
|
name: "selected",
|
|
154
155
|
pointer: "âŻ",
|
|
155
156
|
choices: C
|
|
156
|
-
}, { selected:
|
|
157
|
-
for (let
|
|
158
|
-
if (
|
|
159
|
-
let
|
|
160
|
-
for (let { update:
|
|
157
|
+
}, { selected: i } = await enquirer.prompt(t), a = /* @__PURE__ */ new Set();
|
|
158
|
+
for (let t of i) {
|
|
159
|
+
if (t.startsWith("label|")) {
|
|
160
|
+
let i = t.slice(6), o = x.get(i) ?? [];
|
|
161
|
+
for (let { update: t, index: i } of o) t.latestSha && a.add(i);
|
|
161
162
|
continue;
|
|
162
163
|
}
|
|
163
|
-
let
|
|
164
|
-
Number.isFinite(
|
|
164
|
+
let i = Number.parseInt(t, 10);
|
|
165
|
+
Number.isFinite(i) && a.add(i);
|
|
165
166
|
}
|
|
166
|
-
let
|
|
167
|
-
for (let [
|
|
168
|
-
return
|
|
169
|
-
} catch (
|
|
170
|
-
if (
|
|
171
|
-
throw console.error(pc.red("Unexpected error during selection:"),
|
|
167
|
+
let o = [];
|
|
168
|
+
for (let [t, i] of b.entries()) a.has(t) && i.latestSha && o.push(i);
|
|
169
|
+
return o.length === 0 ? (console.info(pc.yellow("\nNo actions selected")), null) : o;
|
|
170
|
+
} catch (t) {
|
|
171
|
+
if (t instanceof Error && (t.message.includes("cancelled") || t.message.includes("ESC") || t.name === "ExitPromptError")) return logSelectionCancelled(), null;
|
|
172
|
+
throw console.error(pc.red("Unexpected error during selection:"), t), t;
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
181
|
-
} catch {}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
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`;
|
|
175
|
+
function formatAge(t) {
|
|
176
|
+
if (!t) return "";
|
|
177
|
+
let i = Date.now() - t.getTime(), a = Math.floor(i / (1e3 * 60 * 60)), o = Math.floor(a / 24), s = Math.floor(o / 7), c = o % 7;
|
|
178
|
+
return s >= 1 ? c > 0 ? `${s}w ${c}d` : `${s}w` : o >= 1 ? `${o}d` : `${a}h`;
|
|
188
179
|
}
|
|
189
|
-
function formatTableRow(
|
|
190
|
-
let { currentWidth:
|
|
191
|
-
padString(
|
|
192
|
-
padString(
|
|
193
|
-
padString(
|
|
194
|
-
|
|
195
|
-
padString(
|
|
180
|
+
function formatTableRow(t) {
|
|
181
|
+
let { currentWidth: i, actionWidth: a, targetWidth: o, jobWidth: s, ageWidth: l, row: u } = t, d = [
|
|
182
|
+
padString(u.action, a),
|
|
183
|
+
padString(u.job, s),
|
|
184
|
+
padString(u.current, i),
|
|
185
|
+
u.arrow,
|
|
186
|
+
padString(u.target, o)
|
|
196
187
|
];
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
function isSha(e) {
|
|
200
|
-
let a = e.replace(/^v/u, "");
|
|
201
|
-
return /^[0-9a-f]{7,40}$/iu.test(a);
|
|
188
|
+
return l > 0 && d.push(u.age), d.join(" ").replace(/\s+$/u, "");
|
|
202
189
|
}
|
|
203
|
-
function formatVersionOrSha(
|
|
204
|
-
return
|
|
190
|
+
function formatVersionOrSha(t) {
|
|
191
|
+
return t ? isSha(t) ? t.slice(0, 7) : t.replace(/^v/u, "") : pc.gray("unknown");
|
|
205
192
|
}
|
|
206
193
|
function logSelectionCancelled() {
|
|
207
194
|
console.info(`\r\u001B[K${pc.yellow("Selection cancelled")}`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Update level for a version change. */
|
|
2
|
+
type UpdateLevel = 'unknown' | 'major' | 'minor' | 'patch' | 'none';
|
|
3
|
+
/**
|
|
4
|
+
* Determine the update level between two version strings.
|
|
5
|
+
*
|
|
6
|
+
* @param currentVersion - Current version string.
|
|
7
|
+
* @param latestVersion - Latest version string.
|
|
8
|
+
* @returns Update level for the change.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getUpdateLevel(currentVersion: undefined | string | null, latestVersion: undefined | string | null): UpdateLevel;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
function getUpdateLevel(t, r) {
|
|
3
|
+
if (!t || !r) return "unknown";
|
|
4
|
+
let i = normalizeVersion(t), a = normalizeVersion(r);
|
|
5
|
+
if (!i || !a) return "unknown";
|
|
6
|
+
if (semver.eq(i, a)) return "none";
|
|
7
|
+
let o = semver.diff(i, a);
|
|
8
|
+
if (!o) return "none";
|
|
9
|
+
switch (o) {
|
|
10
|
+
case "premajor":
|
|
11
|
+
case "major": return "major";
|
|
12
|
+
case "preminor":
|
|
13
|
+
case "minor": return "minor";
|
|
14
|
+
case "prepatch":
|
|
15
|
+
case "patch": return "patch";
|
|
16
|
+
default: return "unknown";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function normalizeVersion(t) {
|
|
20
|
+
let n = semver.coerce(t);
|
|
21
|
+
return n ? n.version : null;
|
|
22
|
+
}
|
|
23
|
+
export { getUpdateLevel };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort extraction of version number from an inline comment on the same
|
|
3
|
+
* line as `uses:`. Expected shape after update is, for example: `uses:
|
|
4
|
+
* actions/checkout@<sha> # v5.0.0`.
|
|
5
|
+
*
|
|
6
|
+
* Only used when the current reference is a SHA. Returns null if not found.
|
|
7
|
+
*
|
|
8
|
+
* @param filePath - Absolute path to the YAML file.
|
|
9
|
+
* @param lineNumber - 1-based line number of the `uses:` key.
|
|
10
|
+
* @param cache - Optional cache of file contents by path.
|
|
11
|
+
* @returns Extracted version (e.g., `v5.0.0`) or null when not present.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readInlineVersionComment(filePath: undefined | string, lineNumber: undefined | number, cache?: Map<string, string>): Promise<string | null>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
async function readInlineVersionComment(t, n, r) {
|
|
3
|
+
try {
|
|
4
|
+
if (!t || !n || n <= 0) return null;
|
|
5
|
+
let i = r?.get(t);
|
|
6
|
+
i === void 0 && (i = await readFile(t, "utf8"), r && r.set(t, i));
|
|
7
|
+
let a = i.split("\n"), o = n - 1;
|
|
8
|
+
if (o < 0 || o >= a.length) return null;
|
|
9
|
+
let s = a[o].match(/#\s*(?<version>[Vv]?\d+(?:\.\d+){0,2}(?:[+-][\w\-.]+)?)/u);
|
|
10
|
+
if (s?.groups?.version) return s.groups.version;
|
|
11
|
+
} catch {}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export { readInlineVersionComment };
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "1.
|
|
1
|
+
const version = "1.10.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actions-up",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"enquirer": "^2.4.1",
|
|
41
41
|
"nanospinner": "^1.2.2",
|
|
42
42
|
"picocolors": "^1.1.1",
|
|
43
|
-
"semver": "^7.7.
|
|
43
|
+
"semver": "^7.7.4",
|
|
44
44
|
"yaml": "^2.8.2"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
package/readme.md
CHANGED
|
@@ -49,18 +49,7 @@ Interactively upgrade and pin actions to exact commit SHAs for secure, reproduci
|
|
|
49
49
|
|
|
50
50
|
## Why
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Keeping GitHub Actions updated is a critical but tedious task:
|
|
55
|
-
|
|
56
|
-
- **Security Risk**: Using outdated actions with known vulnerabilities
|
|
57
|
-
- **Manual Hell**: Checking dozens of actions across multiple workflows by hand
|
|
58
|
-
- **Version Tags Are Mutable**: v1 or v2 tags can change without notice, breaking reproducibility
|
|
59
|
-
- **Time Sink**: Hours spent on maintenance that could be used for actual development
|
|
60
|
-
|
|
61
|
-
### The Solution
|
|
62
|
-
|
|
63
|
-
Actions Up transforms a painful manual process into a delightful experience:
|
|
52
|
+
Keeping GitHub Actions updated is critical and time-consuming. Actions Up scans all workflows, highlights available updates, and can pin actions to SHAs for reproducibility.
|
|
64
53
|
|
|
65
54
|
| Without Actions Up | With Actions Up |
|
|
66
55
|
| :----------------------------- | :------------------------------- |
|
|
@@ -68,6 +57,18 @@ Actions Up transforms a painful manual process into a delightful experience:
|
|
|
68
57
|
| Risk using vulnerable versions | SHA pinning for maximum security |
|
|
69
58
|
| 30+ minutes per repository | Under 1 minute total |
|
|
70
59
|
|
|
60
|
+
### Security Motivation
|
|
61
|
+
|
|
62
|
+
GitHub Actions run arbitrary code in your CI. If a job has secrets available, any action used in that job can read the environment and exfiltrate those secrets. A compromised action or a mutable version tag is a direct path to leakage.
|
|
63
|
+
|
|
64
|
+
Actions Up reduces risk by:
|
|
65
|
+
|
|
66
|
+
- Pinning actions to commit SHAs to prevent tag hijacking
|
|
67
|
+
- Making outdated actions visible and showing exactly what runs in CI
|
|
68
|
+
- Warning about major updates so you can review changes before applying them
|
|
69
|
+
|
|
70
|
+
Note: secrets are available on `push`, `workflow_dispatch`, `schedule`, and `pull_request_target` triggers (and on fork PRs if explicitly enabled). Always scope workflow permissions to the minimum required.
|
|
71
|
+
|
|
71
72
|
## Installation
|
|
72
73
|
|
|
73
74
|
Quick use (no installation)
|
|
@@ -135,6 +136,15 @@ npx actions-up --dir .gitea
|
|
|
135
136
|
|
|
136
137
|
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
138
|
|
|
139
|
+
### Update Mode
|
|
140
|
+
|
|
141
|
+
By default, Actions Up allows major updates. Use `--mode` to limit updates:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npx actions-up --mode minor
|
|
145
|
+
npx actions-up --mode patch
|
|
146
|
+
```
|
|
147
|
+
|
|
138
148
|
## GitHub Actions Integration
|
|
139
149
|
|
|
140
150
|
### Automated PR Checks
|
|
@@ -154,6 +164,10 @@ jobs:
|
|
|
154
164
|
check-actions:
|
|
155
165
|
name: Check for GHA updates
|
|
156
166
|
runs-on: ubuntu-latest
|
|
167
|
+
permissions:
|
|
168
|
+
contents: read
|
|
169
|
+
pull-requests: write
|
|
170
|
+
issues: write
|
|
157
171
|
steps:
|
|
158
172
|
- name: Checkout repository
|
|
159
173
|
uses: actions/checkout@v4
|
|
@@ -168,7 +182,10 @@ jobs:
|
|
|
168
182
|
|
|
169
183
|
- name: Run actions-up check
|
|
170
184
|
id: actions-check
|
|
185
|
+
env:
|
|
186
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
171
187
|
run: |
|
|
188
|
+
set -euo pipefail
|
|
172
189
|
echo "## GitHub Actions Update Check" >> $GITHUB_STEP_SUMMARY
|
|
173
190
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
174
191
|
|
|
@@ -178,7 +195,7 @@ jobs:
|
|
|
178
195
|
|
|
179
196
|
# Run actions-up and capture output
|
|
180
197
|
echo "Running actions-up to check for updates..."
|
|
181
|
-
actions-up --dry-run > actions-up-raw.txt 2>&1
|
|
198
|
+
actions-up --dry-run > actions-up-raw.txt 2>&1
|
|
182
199
|
|
|
183
200
|
# Parse the output to detect updates
|
|
184
201
|
if grep -q "â" actions-up-raw.txt; then
|
|
@@ -254,7 +271,7 @@ jobs:
|
|
|
254
271
|
fi
|
|
255
272
|
|
|
256
273
|
- name: Comment PR with updates
|
|
257
|
-
if: github.event_name == 'pull_request'
|
|
274
|
+
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
258
275
|
uses: actions/github-script@v7
|
|
259
276
|
with:
|
|
260
277
|
script: |
|
|
@@ -354,34 +371,6 @@ jobs:
|
|
|
354
371
|
|
|
355
372
|
</details>
|
|
356
373
|
|
|
357
|
-
### Scheduled Checks
|
|
358
|
-
|
|
359
|
-
You can also set up scheduled checks to stay informed about updates:
|
|
360
|
-
|
|
361
|
-
```yaml
|
|
362
|
-
name: Weekly Actions Update Check
|
|
363
|
-
|
|
364
|
-
on:
|
|
365
|
-
schedule:
|
|
366
|
-
- cron: '0 9 * * 1' # Every Monday at 9 AM
|
|
367
|
-
workflow_dispatch: # Allow manual triggers
|
|
368
|
-
|
|
369
|
-
jobs:
|
|
370
|
-
check-updates:
|
|
371
|
-
runs-on: ubuntu-latest
|
|
372
|
-
steps:
|
|
373
|
-
- uses: actions/checkout@v4
|
|
374
|
-
- uses: actions/setup-node@v4
|
|
375
|
-
with:
|
|
376
|
-
node-version: '20'
|
|
377
|
-
- run: npm install -g actions-up
|
|
378
|
-
- run: |
|
|
379
|
-
if actions-up --dry-run | grep -q "â"; then
|
|
380
|
-
echo "Updates available! Run 'npx actions-up' to update."
|
|
381
|
-
exit 1
|
|
382
|
-
fi
|
|
383
|
-
```
|
|
384
|
-
|
|
385
374
|
## Example
|
|
386
375
|
|
|
387
376
|
### Regular Actions
|
|
@@ -418,20 +407,12 @@ jobs:
|
|
|
418
407
|
|
|
419
408
|
## Advanced Usage
|
|
420
409
|
|
|
421
|
-
###
|
|
422
|
-
|
|
423
|
-
While Actions Up works without authentication, providing a GitHub token increases API rate limits from 60 to 5000 requests per hour, useful for large projects:
|
|
410
|
+
### GitHub Token
|
|
424
411
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
- For public repositories: Select `public_repo` scope
|
|
428
|
-
- For private repositories: Select `repo` scope
|
|
429
|
-
|
|
430
|
-
Set the token as an environment variable:
|
|
412
|
+
Use `GITHUB_TOKEN` (or a PAT) to raise API rate limits from 60 to 5000 requests/hour.
|
|
431
413
|
|
|
432
414
|
```bash
|
|
433
|
-
|
|
434
|
-
npx actions-up
|
|
415
|
+
GITHUB_TOKEN=your_token_here npx actions-up
|
|
435
416
|
```
|
|
436
417
|
|
|
437
418
|
Or in GitHub Actions:
|
|
@@ -445,48 +426,17 @@ Or in GitHub Actions:
|
|
|
445
426
|
|
|
446
427
|
### Skipping Updates
|
|
447
428
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
#### CLI Excludes
|
|
451
|
-
|
|
452
|
-
Skip actions by name using regular expressions. Patterns are matched against the full action name (`owner/repo[/path]`).
|
|
453
|
-
|
|
454
|
-
- Repeatable flag: `--exclude <regex>` (can be used multiple times)
|
|
455
|
-
- Comma-separated list is supported inside a single flag
|
|
456
|
-
- Forms:
|
|
457
|
-
- Plain string compiled as case-insensitive regex: `my-org/.*`
|
|
458
|
-
- Literal with flags: `/^actions\/internal-.+$/i`
|
|
459
|
-
|
|
460
|
-
Examples:
|
|
429
|
+
Use CLI excludes or YAML ignore comments.
|
|
461
430
|
|
|
462
431
|
```bash
|
|
463
|
-
npx actions-up --exclude "my-org/.*"
|
|
464
|
-
npx actions-up --exclude ".*/internal-.*" --exclude "/^acme\/.+$/i"
|
|
465
|
-
# or
|
|
466
|
-
npx actions-up --exclude "my-org/.*, .*/internal-.*"
|
|
432
|
+
npx actions-up --exclude "my-org/.*" --exclude ".*/internal-.*"
|
|
467
433
|
```
|
|
468
434
|
|
|
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
435
|
```bash
|
|
474
|
-
# Only show updates released at least 7 days ago
|
|
475
436
|
npx actions-up --min-age 7
|
|
476
437
|
```
|
|
477
438
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
#### Ignore Comments
|
|
481
|
-
|
|
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`.
|
|
483
|
-
|
|
484
|
-
- Ignore whole file: `# actions-up-ignore-file`
|
|
485
|
-
- Block ignore: `# actions-up-ignore-start` ⌠`# actions-up-ignore-end`
|
|
486
|
-
- Next line: `# actions-up-ignore-next-line`
|
|
487
|
-
- Inline on the same line: append `# actions-up-ignore`
|
|
488
|
-
|
|
489
|
-
Example:
|
|
439
|
+
Ignore comments (file/block/next-line/inline):
|
|
490
440
|
|
|
491
441
|
```yaml
|
|
492
442
|
# actions-up-ignore-file
|
|
@@ -501,23 +451,14 @@ Example:
|
|
|
501
451
|
# actions-up-ignore-end
|
|
502
452
|
```
|
|
503
453
|
|
|
504
|
-
##
|
|
505
|
-
|
|
506
|
-
Actions Up promotes security best practices:
|
|
507
|
-
|
|
508
|
-
- **SHA pinning**: Uses commit SHA instead of mutable tags
|
|
509
|
-
- **Version comment**: Adds the released version next to the pinned SHA for readability
|
|
510
|
-
- **No Auto-Updates**: Full control over what gets updated
|
|
511
|
-
- **Breaking Change Warnings**: Alerts you to major version updates that may require configuration changes
|
|
512
|
-
|
|
513
|
-
## CI/CD Best Practices
|
|
454
|
+
## Why Actions Up?
|
|
514
455
|
|
|
515
|
-
|
|
456
|
+
Interactive CLI for developers who want control over GitHub Actions updates.
|
|
516
457
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
458
|
+
- **vs. Dependabot/Renovate:** Dependabot and Renovate update via pull requests; Actions Up is an interactive CLI with explicit SHA pinning.
|
|
459
|
+
- **vs. pinact:** pinact is a CLI to pin and update Actions and reusable workflows; Actions Up adds interactive selection and major update warnings.
|
|
460
|
+
- **Zero-config:** `npx actions-up` runs immediately.
|
|
461
|
+
- **Breaking change warnings:** Major updates are flagged before applying.
|
|
521
462
|
|
|
522
463
|
## Contributing
|
|
523
464
|
|