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 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 l = cac("actions-up");
14
- l.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("--yes, -y", "Skip all confirmations").command("", "Update GitHub Actions").action(async (s) => {
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 c = createSpinner("Scanning GitHub Actions...").start();
16
+ let l = createSpinner("Scanning GitHub Actions...").start();
17
17
  try {
18
- let l = await scanGitHubActions(process.cwd(), s.dir), u = l.actions.length, d = l.workflows.size, f = l.compositeActions.size;
19
- if (c.success(`Found ${pc.yellow(u)} actions in ${pc.yellow(d)} workflows and ${pc.yellow(f)} composite actions`), u === 0) {
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 p = l.actions, m = [];
24
- Array.isArray(s.exclude) ? m.push(...s.exclude) : typeof s.exclude == "string" && m.push(s.exclude);
25
- let h = m.flatMap((e) => e.split(",")).map((e) => e.trim()).filter(Boolean);
26
- if (h.length > 0) {
27
- let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"), a = e(h);
28
- a.length > 0 && (p = p.filter((e) => {
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 (c = createSpinner("Checking for updates...").start(), p.length === 0) {
35
- c.success("No actions to check after excludes"), console.info(pc.green("\n✨ Nothing to check after excludes\n"));
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 g = await checkUpdates(p, process.env.GITHUB_TOKEN), _ = [];
39
- await Promise.all(g.map(async (e) => {
40
- await shouldIgnore(e.action.file, e.action.line) || _.push(e);
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 v = _.filter((e) => e.hasUpdate), y = v.filter((e) => e.isBreaking);
43
- if (v.length === 0) {
44
- c.success("All actions are up to date!"), console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
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 (c.success(`Found ${pc.yellow(v.length)} updates available${y.length > 0 ? ` (${pc.redBright(y.length)} breaking)` : ""}`), s.dryRun) {
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 v) 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`);
50
- console.info(pc.gray(`\n${v.length} actions would be updated\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 (s.yes) {
54
- let e = v.filter((e) => e.latestSha);
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
- c.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);
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
- }), l.parse();
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): Promise<ActionUpdate[]>;
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(r, a) {
4
- let c = createGitHubClient(a), l = r.filter((e) => e.type === "external");
5
- if (l.length === 0) return [];
6
- let u = /* @__PURE__ */ new Map();
7
- for (let e of l) {
8
- let n = u.get(e.name) ?? [];
9
- n.push(e), u.set(e.name, n);
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 d = {
11
+ let p = {
12
12
  rateLimitError: null,
13
13
  rateLimitHit: !1
14
- }, f = await [...u.keys()].reduce((e, r) => e.then(async (e) => {
15
- if (d.rateLimitHit) return [...e, {
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: r,
18
+ actionName: n,
18
19
  sha: null
19
20
  }];
20
- let i = r.split("/");
21
- if (i.length < 2) return [...e, {
21
+ let r = n.split("/");
22
+ if (r.length < 2) return [...e, {
23
+ publishedAt: null,
22
24
  version: null,
23
- actionName: r,
25
+ actionName: n,
24
26
  sha: null
25
27
  }];
26
- let [a, l] = i;
27
- if (!a || !l) return [...e, {
28
+ let [i, c] = r;
29
+ if (!i || !c) return [...e, {
30
+ publishedAt: null,
28
31
  version: null,
29
- actionName: r,
32
+ actionName: n,
30
33
  sha: null
31
34
  }];
32
35
  try {
33
- let i = u.get(r)[0]?.version;
34
- if (i && !isSha(i) && !isSemverLike(i) && await c.getRefType(a, l, i) === "branch") return [...e, {
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: r,
42
+ actionName: n,
37
43
  sha: null
38
44
  }];
39
- let d = await c.getLatestRelease(a, l);
45
+ let d = await l.getLatestRelease(i, c);
40
46
  if (!d) {
41
- let e = await c.getAllReleases(a, l, 1);
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: i, sha: o } = d, u = !1;
51
+ let { publishedAt: r, version: o, sha: u } = d, f = !1;
46
52
  {
47
- let e = normalizeVersion(i), r = !!(i && i.trim() !== ""), a = r && /^v?\d+$/u.test(i.trim()), o = semver.valid(e);
48
- u = !r || a || !o;
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 (u) {
51
- let o = await c.getAllTags(a, l, 30);
52
- if (o.length > 0) {
53
- let u = o.filter((e) => isSemverLike(e.tag)).map((e) => ({
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, r) => {
59
- let i = semver.rcompare(e.v, r.v);
60
- if (i !== 0) return i;
61
- let a = /\d+\.\d+/u.test(e.raw.tag) ? 1 : 0;
62
- return (/\d+\.\d+/u.test(r.raw.tag) ? 1 : 0) - a;
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 o = u[0].raw, s = semver.valid(normalizeVersion(i) ?? void 0);
65
- if (!s || semver.gt(u[0].v, s) || semver.eq(u[0].v, s) && /\d+\.\d+/u.test(o.tag)) {
66
- let n = o.tag, i = o.sha?.length ? o.sha : null;
67
- if (!i && n) try {
68
- i = await c.getTagSha(a, l, n);
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: n,
72
- sha: i,
73
- actionName: r
77
+ version: t,
78
+ publishedAt: null,
79
+ sha: a,
80
+ actionName: n
74
81
  }];
75
82
  }
76
83
  }
77
84
  }
78
85
  }
79
- if (!o && i) try {
80
- o = await c.getTagSha(a, l, i);
86
+ if (!u && o) try {
87
+ u = await l.getTagSha(i, c, o);
81
88
  } catch {}
82
89
  return [...e, {
83
- actionName: r,
84
- version: i,
85
- sha: o
90
+ status: "ok",
91
+ publishedAt: r,
92
+ actionName: n,
93
+ version: o,
94
+ sha: u
86
95
  }];
87
96
  }
88
- let f = await c.getAllTags(a, l, 30);
89
- if (f.length > 0) {
90
- let i = f.filter((e) => isSemverLike(e.tag)).map((e) => ({
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
- i.length > 0 ? (i.sort((e, r) => {
95
- let i = semver.rcompare(e.v, r.v);
96
- if (i !== 0) return i;
97
- let a = /\d+\.\d+/u.test(e.raw.tag) ? 1 : 0;
98
- return (/\d+\.\d+/u.test(r.raw.tag) ? 1 : 0) - a;
99
- }), o = i[0].raw) : o = f[0];
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 c.getTagSha(a, l, u);
111
+ d = await l.getTagSha(i, c, u);
103
112
  } catch {}
104
113
  return [...e, {
105
- actionName: r,
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: r,
124
+ actionName: n,
113
125
  sha: null
114
126
  }];
115
- } catch (n) {
116
- return n instanceof Error && n.name === "GitHubRateLimitError" ? (d.rateLimitHit = !0, d.rateLimitError = n, [...e, {
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: r,
131
+ actionName: n,
119
132
  sha: null
120
- }]) : (console.warn(`Failed to check ${r}:`, n), [...e, {
133
+ }]) : (console.warn(`Failed to check ${n}:`, t), [...e, {
134
+ publishedAt: null,
121
135
  version: null,
122
- actionName: r,
136
+ actionName: n,
123
137
  sha: null
124
138
  }]);
125
139
  }
126
140
  }), Promise.resolve([]));
127
- if (d.rateLimitError) {
128
- let e = !!(a ?? process.env.GITHUB_TOKEN), n = `${d.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"}`, r = Error(n);
129
- throw r.name = "GitHubRateLimitError", r;
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 p = /* @__PURE__ */ new Map();
132
- for (let e of f) p.set(e.actionName, {
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 m = [];
137
- for (let e of l) {
138
- let n = p.get(e.name);
139
- n ? m.push(createUpdate(e, n.version, n.sha)) : m.push(createUpdate(e, null, null));
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 m;
170
+ return g;
142
171
  }
143
- function createUpdate(e, r, i) {
144
- let s = normalizeVersion(e.version ?? ""), c = r ? normalizeVersion(r) : null, l = !1, u = !1;
145
- if (s && isSha(s)) i ? l = !compareSha(s, i) : c && (l = !0);
146
- else if (s && c) {
147
- let r = semver.valid(s), a = semver.valid(c);
148
- if (r && a) {
149
- if (l = semver.lt(r, a), l) {
150
- let e = semver.major(r);
151
- u = semver.major(a) > e;
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
- !l && semver.eq(r, a) && !isSha(e.version) && i && (l = !0, u = !1);
154
- } else s !== c && (l = !0);
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: e.version ?? "unknown",
158
- latestVersion: r,
159
- isBreaking: u,
160
- latestSha: i,
161
- hasUpdate: l,
162
- action: e
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, n) {
166
- let r = e.replace(/^v/u, ""), i = n.replace(/^v/u, ""), a = Math.min(r.length, i.length);
167
- return a < 7 ? !1 : r.slice(0, Math.max(0, a)).toLowerCase() === i.slice(0, Math.max(0, a)).toLowerCase();
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 r = e.replace(/^v/u, "");
172
- if (/^[0-9a-f]{7,40}$/iu.test(r)) return e;
173
- let i = semver.coerce(r);
174
- return i ? i.version : e;
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 n = e.replace(/^v/u, "");
179
- return /^[0-9a-f]{7,40}$/iu.test(n);
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 n = e.trim();
184
- return /^v?\d+(?:\.\d+){0,2}$/u.test(n);
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(d.value, s, o) : [];
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(o, s, c) {
8
- if (!isWorkflowStructure(o.toJSON()) || !o.contents || !isYAMLMap(o.contents)) return [];
9
- let l = findMapPair(o.contents, "jobs");
10
- if (!l?.value || !isYAMLMap(l.value)) return [];
11
- let u = [];
12
- for (let e of l.value.items) {
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 o = findMapPair(e.value, "steps");
15
- o?.value && u.push(...extractUsesFromSteps(o.value, c, s));
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 u;
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 stepsNode - YAML sequence node containing workflow/action steps.
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(stepsNode: unknown, filePath: string, content: string): GitHubAction[];
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, c, l) {
9
- if (!isYAMLSequence(s)) return [];
10
- let u = [];
11
- for (let o of s.items) {
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 d = s;
16
- if (typeof d.uses != "string") continue;
17
- let f = o.items.find((e) => isPair(e) && isScalar(e.key) && e.key.value === "uses"), p = f?.key ? getLineNumberForKey(l, f.key) : 0, m = parseActionReference(d.uses, c, p);
18
- m && u.push(m);
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 u;
21
+ return f;
21
22
  }
22
23
  export { extractUsesFromSteps };
@@ -1,2 +1,7 @@
1
1
  import { ActionUpdate } from '../../types/action-update';
2
- export declare function promptUpdateSelection(updates: ActionUpdate[]): Promise<ActionUpdate[] | null>;
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 = 56, MIN_CURRENT_WIDTH = 16;
11
- async function promptUpdateSelection(o) {
12
- if (o.length === 0) return null;
13
- let c = o.filter((e) => e.hasUpdate);
14
- if (c.length === 0) return console.info(pc.green("✓ All actions are up to date!")), null;
15
- let p = /* @__PURE__ */ new Map();
16
- for (let [e, a] of c.entries()) {
17
- let o = a.action.file ?? "unknown file", s = path.relative(path.join(process.cwd(), GITHUB_DIRECTORY), o);
18
- s === "" && (s = o);
19
- let c = p.get(s) ?? [];
20
- c.push({
21
- update: a,
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
- }), p.set(s, c);
24
+ }), x.set(c, l);
24
25
  }
25
- let g = await Promise.all(c.map(async (e) => {
26
- let i = formatVersionOrSha(e.currentVersion), a = e.currentVersion ?? void 0;
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
- effectiveForDiff: a,
29
- display: i
29
+ versionForPadding: s,
30
+ effectiveForDiff: o,
31
+ shortSha: c,
32
+ display: a
30
33
  };
31
- let o = await tryReadInlineVersionComment(e.action.file, e.action.line);
32
- if (o) {
33
- let c = e.currentVersion.slice(0, 7);
34
- i = `${formatVersionOrSha(o)} ${pc.gray(`(${c})`)}`, a = o;
35
- }
36
- return {
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
- })), _ = [], v = stripAnsi("Action").length, y = stripAnsi("Current").length;
41
- for (let [e, i] of c.entries()) {
42
- let o = i.action.name, s = g[e].display;
43
- v = Math.max(v, o.length), y = Math.max(y, stripAnsi(s).length);
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 b = Math.max(v, MIN_ACTION_WIDTH), x = Math.max(y, MIN_CURRENT_WIDTH), S = [...p.keys()].toSorted();
46
- for (let [i, a] of S.entries()) {
47
- let o = p.get(a);
48
- if (!o) {
49
- console.warn(`Unexpected missing group for file: ${a}`);
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 c = [], l = o;
53
- c.push({
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: i, index: a } of l) {
60
- let o = !!i.latestSha, l = g[a].display, u = g[a]?.effectiveForDiff ?? i.currentVersion, d = formatVersion(i.latestVersion, u), f = i.action.name;
61
- if (i.latestSha) {
62
- let e = i.latestSha.slice(0, 7);
63
- d = `${d} ${pc.gray(`(${e})`)}`;
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
- o || (d = pc.gray(d), l = pc.gray(l), f = pc.gray(f)), c.push({
66
- action: f,
67
- target: d,
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: l
83
+ current: f
70
84
  });
71
85
  }
72
- let u = Math.max(b, MIN_ACTION_WIDTH), m = Math.max(x, MIN_CURRENT_WIDTH), h = [];
73
- for (let [e, i] of c.entries()) {
74
- let a = e === 0, o = formatTableRow(i, u, m);
75
- if (a) h.push({
76
- message: pc.gray(` ○ ${o}`),
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: i, index: a } = l[e - 1], s = !!i.latestSha, c = s && !i.isBreaking;
83
- h.push({
84
- message: o,
85
- value: String(a),
86
- name: String(a),
87
- disabled: !s,
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: c
110
+ enabled: l
90
111
  });
91
112
  }
92
113
  }
93
- _.push({
94
- message: pc.gray(a),
95
- value: `label|${a}`,
96
- choices: h,
97
- name: `label|${a}`,
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
- }), i < S.length - 1 && _.push({
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, i) {
109
- if (i.isGroupLabel) {
110
- let e = (i.choices ?? []).filter((e) => !("role" in e)), a = e.length, o = e.filter((e) => !!e.enabled).length === a ? "●" : "○";
111
- return ` ${pc.gray(o)}`;
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 ` ${i.enabled ? "●" : "○"}`;
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: i } = await enquirer.prompt(e), a = /* @__PURE__ */ new Set();
136
- for (let e of i) {
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 i = e.slice(6), o = p.get(i) ?? [];
139
- for (let { update: e, index: i } of o) e.latestSha && a.add(i);
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 i = Number.parseInt(e, 10);
143
- Number.isFinite(i) && a.add(i);
163
+ let a = Number.parseInt(e, 10);
164
+ Number.isFinite(a) && o.add(a);
144
165
  }
145
- let o = [];
146
- for (let [e, i] of c.entries()) a.has(e) && i.latestSha && o.push(i);
147
- return o.length === 0 ? (console.info(pc.yellow("\nNo actions selected")), null) : o;
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, i) {
174
+ async function tryReadInlineVersionComment(e, a) {
154
175
  try {
155
- if (!e || !i || i <= 0) return null;
156
- let a = (await readFile(e, "utf8")).split("\n"), o = i - 1;
157
- if (o < 0 || o >= a.length) return null;
158
- let s = a[o].match(/#\s*(?<version>[Vv]?\d+(?:\.\d+){0,2}(?:[+-][\w\-.]+)?)/u);
159
- if (s?.groups?.version) return s.groups.version;
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 formatTableRow(e, i, a) {
164
- return [
165
- padString(e.action, i),
166
- padString(e.current, a),
167
- e.arrow,
168
- e.target
169
- ].join(" ").replace(/\s+$/u, "");
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 i = e.replace(/^v/u, "");
173
- return /^[0-9a-f]{7,40}$/iu.test(i);
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.5.0";
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.5.0",
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.1"
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`.