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 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 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) => {
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 l = createSpinner("Scanning GitHub Actions...").start();
19
+ let m = createSpinner("Scanning GitHub Actions...").start();
17
20
  try {
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) {
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 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
- let { name: o } = e;
30
- for (let e of a) if (e.test(o)) return !1;
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 (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"));
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 _ = 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
+ 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 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
+ 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 (l.success(`Found ${pc.yellow(x.length)} updates available${w.length > 0 ? ` (${pc.redBright(w.length)} breaking)` : ""}`), b.length > 0 && printSkippedWarning(b, _), c.dryRun) {
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 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`));
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 (c.yes) {
56
- let e = x.filter((e) => e.latestSha);
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
- let o = await promptUpdateSelection(x, { showAge: c.minAge > 0 });
64
- if (!o || o.length === 0) {
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 ${o.length} selected actions...\n`)), await applyUpdates(o), console.info(pc.green("\n✓ Updates applied successfully!"));
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
- 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);
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
- }), u.parse();
94
+ }), h.parse();
74
95
  }
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"}`;
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
- console.info("");
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(l, g = {}) {
12
- let { showAge: y = !1 } = g;
13
- if (l.length === 0) return null;
14
- let b = l.filter((e) => e.hasUpdate);
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 [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,
23
- index: e
24
- }), x.set(c, l);
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 (e) => {
27
- let a = formatVersionOrSha(e.currentVersion), o = e.currentVersion ?? void 0, s = null, c = null;
28
- if (!e.currentVersion || !isSha(e.currentVersion)) return {
29
- versionForPadding: s,
30
- effectiveForDiff: o,
31
- shortSha: c,
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 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,
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 [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);
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 m = S[a]?.versionForPadding;
49
- m && (D = Math.max(D, stripAnsi(m).length)), l.publishedAt && (O = !0);
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 [a, o] of F.entries()) {
53
- let l = x.get(o);
54
- if (!l) {
55
- console.warn(`Unexpected missing group for file: ${o}`);
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 u = [], d = l;
59
- u.push({
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: 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})`)}`;
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
- 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,
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: f
84
+ current: d
84
85
  });
85
86
  }
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({
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: g,
91
- actionWidth: h,
91
+ currentWidth: h,
92
+ actionWidth: d,
92
93
  ageWidth: P,
93
- jobWidth: _,
94
- row: a
94
+ jobWidth: g,
95
+ row: i
95
96
  });
96
- if (o) v.push({
97
- message: pc.gray(` ○ ${s}`),
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: 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,
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: l
111
+ enabled: c
111
112
  });
112
113
  }
113
114
  }
114
115
  C.push({
115
- message: pc.gray(o),
116
- value: `label|${o}`,
117
- choices: v,
118
- name: `label|${o}`,
116
+ message: pc.gray(a),
117
+ value: `label|${a}`,
118
+ choices: _,
119
+ name: `label|${a}`,
119
120
  isGroupLabel: !0,
120
121
  enabled: !1
121
- }), a < F.length - 1 && C.push({
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 e = {
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)}`;
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 ` ${a.enabled ? "●" : "○"}`;
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: a } = await enquirer.prompt(e), o = /* @__PURE__ */ new Set();
157
- for (let e of a) {
158
- if (e.startsWith("label|")) {
159
- let a = e.slice(6), s = x.get(a) ?? [];
160
- for (let { update: e, index: a } of s) e.latestSha && o.add(a);
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 a = Number.parseInt(e, 10);
164
- Number.isFinite(a) && o.add(a);
164
+ let i = Number.parseInt(t, 10);
165
+ Number.isFinite(i) && a.add(i);
165
166
  }
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;
169
- } catch (e) {
170
- if (e instanceof Error && (e.message.includes("cancelled") || e.message.includes("ESC") || e.name === "ExitPromptError")) return logSelectionCancelled(), null;
171
- throw console.error(pc.red("Unexpected error during selection:"), e), e;
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
- async function tryReadInlineVersionComment(e, a) {
175
- try {
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;
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(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)
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 u > 0 && f.push(d.age), f.join(" ").replace(/\s+$/u, "");
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(e) {
204
- return e ? isSha(e) ? e.slice(0, 7) : e.replace(/^v/u, "") : pc.gray("unknown");
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,7 @@
1
+ /**
2
+ * Check if a string is a Git SHA hash.
3
+ *
4
+ * @param value - String to check.
5
+ * @returns True if the string is a SHA hash.
6
+ */
7
+ export declare function isSha(value: undefined | string | null): boolean;
@@ -0,0 +1,6 @@
1
+ function isSha(e) {
2
+ if (!e) return !1;
3
+ let t = e.replace(/^v/u, "");
4
+ return /^[0-9a-f]{7,40}$/iu.test(t);
5
+ }
6
+ export { isSha };
@@ -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.9.0";
1
+ const version = "1.10.0";
2
2
  export { version };
@@ -0,0 +1,2 @@
1
+ /** Allowed update modes for filtering actions. */
2
+ export type UpdateMode = 'major' | 'minor' | 'patch'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actions-up",
3
- "version": "1.9.0",
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.3",
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
- ### The Problem
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 || true
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
- ### Using GitHub Token for Higher Rate Limits
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
- [Create a GitHub Personal Access Token](https://github.com/settings/tokens/new?scopes=public_repo&description=actions-up).
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
- export GITHUB_TOKEN=your_token_here
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
- Skip updates using CLI excludes and YAML ignore comments. Excludes run first, then ignore comments.
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
- When `--min-age` is set, an "Age" column appears showing how long ago each release was published (e.g., `3d`, `1w 2d`).
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
- ## Security
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
- When using Actions Up in your CI/CD pipeline:
456
+ Interactive CLI for developers who want control over GitHub Actions updates.
516
457
 
517
- 1. **Start with warnings**: Begin by running checks without failing builds to gauge the update frequency
518
- 2. **Regular updates**: Schedule weekly or monthly update PRs rather than blocking every PR
519
- 3. **Team education**: Ensure your team understands the security benefits of keeping actions updated
520
- 4. **Gradual adoption**: Roll out to a few repositories first before organization-wide deployment
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