actions-up 1.5.0 ā 1.6.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 +6 -4
- package/dist/core/api/check-updates.js +107 -87
- package/dist/core/ast/scanners/scan-composite-action-ast.js +5 -1
- package/dist/core/ast/scanners/scan-workflow-ast.d.ts +2 -1
- package/dist/core/ast/scanners/scan-workflow-ast.js +22 -9
- package/dist/core/ast/utils/extract-uses-from-steps.d.ts +13 -4
- package/dist/core/ast/utils/extract-uses-from-steps.js +10 -9
- package/dist/core/interactive/prompt-update-selection.d.ts +6 -1
- package/dist/core/interactive/prompt-update-selection.js +122 -94
- package/dist/core/parsing/parse-action-reference.js +1 -1
- package/dist/package.js +1 -1
- package/dist/types/action-update.d.ts +3 -0
- package/dist/types/github-action.d.ts +4 -1
- package/dist/types/workflow-job.d.ts +9 -0
- package/package.json +1 -1
- package/readme.md +34 -0
package/dist/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import pc from "picocolors";
|
|
|
11
11
|
import cac from "cac";
|
|
12
12
|
function run() {
|
|
13
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) => {
|
|
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("--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 (s) => {
|
|
15
15
|
console.info(pc.cyan("\nš Actions Up!\n"));
|
|
16
16
|
let c = createSpinner("Scanning GitHub Actions...").start();
|
|
17
17
|
try {
|
|
@@ -39,12 +39,14 @@ function run() {
|
|
|
39
39
|
await Promise.all(g.map(async (e) => {
|
|
40
40
|
await shouldIgnore(e.action.file, e.action.line) || _.push(e);
|
|
41
41
|
}));
|
|
42
|
-
let v = _.filter((e) => e.hasUpdate), y =
|
|
42
|
+
let v = _.filter((e) => e.hasUpdate), y = s.minAge * 24 * 60 * 60 * 1e3, b = Date.now();
|
|
43
|
+
v = v.filter((e) => e.publishedAt ? b - e.publishedAt.getTime() >= y : !0);
|
|
44
|
+
let x = v.filter((e) => e.isBreaking);
|
|
43
45
|
if (v.length === 0) {
|
|
44
46
|
c.success("All actions are up to date!"), 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${
|
|
49
|
+
if (c.success(`Found ${pc.yellow(v.length)} updates available${x.length > 0 ? ` (${pc.redBright(x.length)} breaking)` : ""}`), s.dryRun) {
|
|
48
50
|
console.info(pc.yellow("\nš Dry Run - No changes will be made\n"));
|
|
49
51
|
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
52
|
console.info(pc.gray(`\n${v.length} actions would be updated\n`));
|
|
@@ -58,7 +60,7 @@ function run() {
|
|
|
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(_, { showAge: s.minAge > 0 });
|
|
62
64
|
if (!o || o.length === 0) {
|
|
63
65
|
console.info(pc.gray("\nNo updates applied"));
|
|
64
66
|
return;
|
|
@@ -1,186 +1,206 @@
|
|
|
1
1
|
import { createGitHubClient } from "./create-github-client.js";
|
|
2
2
|
import semver from "semver";
|
|
3
|
-
async function checkUpdates(
|
|
4
|
-
let c = createGitHubClient(
|
|
3
|
+
async function checkUpdates(n, i) {
|
|
4
|
+
let c = createGitHubClient(i), l = n.filter((e) => e.type === "external" || e.type === "reusable-workflow");
|
|
5
5
|
if (l.length === 0) return [];
|
|
6
6
|
let u = /* @__PURE__ */ new Map();
|
|
7
7
|
for (let e of l) {
|
|
8
|
-
let
|
|
9
|
-
|
|
8
|
+
let t = u.get(e.name) ?? [];
|
|
9
|
+
t.push(e), u.set(e.name, t);
|
|
10
10
|
}
|
|
11
11
|
let d = {
|
|
12
12
|
rateLimitError: null,
|
|
13
13
|
rateLimitHit: !1
|
|
14
|
-
}, f = await [...u.keys()].reduce((e,
|
|
14
|
+
}, f = await [...u.keys()].reduce((e, n) => e.then(async (e) => {
|
|
15
15
|
if (d.rateLimitHit) return [...e, {
|
|
16
|
+
publishedAt: null,
|
|
16
17
|
version: null,
|
|
17
|
-
actionName:
|
|
18
|
+
actionName: n,
|
|
18
19
|
sha: null
|
|
19
20
|
}];
|
|
20
|
-
let
|
|
21
|
-
if (
|
|
21
|
+
let r = n.split("/");
|
|
22
|
+
if (r.length < 2) return [...e, {
|
|
23
|
+
publishedAt: null,
|
|
22
24
|
version: null,
|
|
23
|
-
actionName:
|
|
25
|
+
actionName: n,
|
|
24
26
|
sha: null
|
|
25
27
|
}];
|
|
26
|
-
let [
|
|
27
|
-
if (!
|
|
28
|
+
let [i, l] = r;
|
|
29
|
+
if (!i || !l) return [...e, {
|
|
30
|
+
publishedAt: null,
|
|
28
31
|
version: null,
|
|
29
|
-
actionName:
|
|
32
|
+
actionName: n,
|
|
30
33
|
sha: null
|
|
31
34
|
}];
|
|
32
35
|
try {
|
|
33
|
-
let
|
|
34
|
-
if (
|
|
36
|
+
let r = u.get(n)[0]?.version;
|
|
37
|
+
if (r && !isSha(r) && !isSemverLike(r) && await c.getRefType(i, l, r) === "branch") return [...e, {
|
|
38
|
+
publishedAt: null,
|
|
35
39
|
version: null,
|
|
36
|
-
actionName:
|
|
40
|
+
actionName: n,
|
|
37
41
|
sha: null
|
|
38
42
|
}];
|
|
39
|
-
let d = await c.getLatestRelease(
|
|
43
|
+
let d = await c.getLatestRelease(i, l);
|
|
40
44
|
if (!d) {
|
|
41
|
-
let e = await c.getAllReleases(
|
|
45
|
+
let e = await c.getAllReleases(i, l, 1);
|
|
42
46
|
d = e.find((e) => !e.isPrerelease) ?? e[0] ?? null;
|
|
43
47
|
}
|
|
44
48
|
if (d) {
|
|
45
|
-
let { version:
|
|
49
|
+
let { publishedAt: r, version: o, sha: u } = d, f = !1;
|
|
46
50
|
{
|
|
47
|
-
let e = normalizeVersion(
|
|
48
|
-
|
|
51
|
+
let e = normalizeVersion(o), n = !!(o && o.trim() !== ""), r = n && /^v?\d+$/u.test(o.trim()), i = semver.valid(e);
|
|
52
|
+
f = !n || r || !i;
|
|
49
53
|
}
|
|
50
|
-
if (
|
|
51
|
-
let
|
|
52
|
-
if (
|
|
53
|
-
let u =
|
|
54
|
+
if (f) {
|
|
55
|
+
let r = await c.getAllTags(i, l, 30);
|
|
56
|
+
if (r.length > 0) {
|
|
57
|
+
let u = r.filter((e) => isSemverLike(e.tag)).map((e) => ({
|
|
54
58
|
v: semver.valid(normalizeVersion(e.tag)),
|
|
55
59
|
raw: e
|
|
56
60
|
}));
|
|
57
61
|
if (u.length > 0) {
|
|
58
|
-
u.sort((e,
|
|
59
|
-
let
|
|
60
|
-
if (
|
|
61
|
-
let
|
|
62
|
-
return (/\d+\.\d+/u.test(
|
|
62
|
+
u.sort((e, n) => {
|
|
63
|
+
let r = semver.rcompare(e.v, n.v);
|
|
64
|
+
if (r !== 0) return r;
|
|
65
|
+
let i = /\d+\.\d+/u.test(e.raw.tag) ? 1 : 0;
|
|
66
|
+
return (/\d+\.\d+/u.test(n.raw.tag) ? 1 : 0) - i;
|
|
63
67
|
});
|
|
64
|
-
let
|
|
65
|
-
if (!s || semver.gt(u[0].v, s) || semver.eq(u[0].v, s) && /\d+\.\d+/u.test(
|
|
66
|
-
let
|
|
67
|
-
if (!
|
|
68
|
-
|
|
68
|
+
let r = u[0].raw, s = semver.valid(normalizeVersion(o) ?? void 0);
|
|
69
|
+
if (!s || semver.gt(u[0].v, s) || semver.eq(u[0].v, s) && /\d+\.\d+/u.test(r.tag)) {
|
|
70
|
+
let t = r.tag, a = r.sha?.length ? r.sha : null;
|
|
71
|
+
if (!a && t) try {
|
|
72
|
+
a = await c.getTagSha(i, l, t);
|
|
69
73
|
} catch {}
|
|
70
74
|
return [...e, {
|
|
71
|
-
version:
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
version: t,
|
|
76
|
+
publishedAt: null,
|
|
77
|
+
sha: a,
|
|
78
|
+
actionName: n
|
|
74
79
|
}];
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
|
-
if (!
|
|
80
|
-
|
|
84
|
+
if (!u && o) try {
|
|
85
|
+
u = await c.getTagSha(i, l, o);
|
|
81
86
|
} catch {}
|
|
82
87
|
return [...e, {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
publishedAt: r,
|
|
89
|
+
actionName: n,
|
|
90
|
+
version: o,
|
|
91
|
+
sha: u
|
|
86
92
|
}];
|
|
87
93
|
}
|
|
88
|
-
let f = await c.getAllTags(
|
|
94
|
+
let f = await c.getAllTags(i, l, 30);
|
|
89
95
|
if (f.length > 0) {
|
|
90
|
-
let
|
|
96
|
+
let r = f.filter((e) => isSemverLike(e.tag)).map((e) => ({
|
|
91
97
|
v: semver.valid(normalizeVersion(e.tag)),
|
|
92
98
|
raw: e
|
|
93
99
|
})), o;
|
|
94
|
-
|
|
95
|
-
let
|
|
96
|
-
if (
|
|
97
|
-
let
|
|
98
|
-
return (/\d+\.\d+/u.test(
|
|
99
|
-
}), o =
|
|
100
|
+
r.length > 0 ? (r.sort((e, n) => {
|
|
101
|
+
let r = semver.rcompare(e.v, n.v);
|
|
102
|
+
if (r !== 0) return r;
|
|
103
|
+
let i = /\d+\.\d+/u.test(e.raw.tag) ? 1 : 0;
|
|
104
|
+
return (/\d+\.\d+/u.test(n.raw.tag) ? 1 : 0) - i;
|
|
105
|
+
}), o = r[0].raw) : o = f[0];
|
|
100
106
|
let u = o.tag, d = o.sha?.length ? o.sha : null;
|
|
101
107
|
if (!d && u) try {
|
|
102
|
-
d = await c.getTagSha(
|
|
108
|
+
d = await c.getTagSha(i, l, u);
|
|
103
109
|
} catch {}
|
|
104
110
|
return [...e, {
|
|
105
|
-
|
|
111
|
+
publishedAt: null,
|
|
112
|
+
actionName: n,
|
|
106
113
|
version: u,
|
|
107
114
|
sha: d
|
|
108
115
|
}];
|
|
109
116
|
}
|
|
110
117
|
return [...e, {
|
|
118
|
+
publishedAt: null,
|
|
111
119
|
version: null,
|
|
112
|
-
actionName:
|
|
120
|
+
actionName: n,
|
|
113
121
|
sha: null
|
|
114
122
|
}];
|
|
115
|
-
} catch (
|
|
116
|
-
return
|
|
123
|
+
} catch (t) {
|
|
124
|
+
return t instanceof Error && t.name === "GitHubRateLimitError" ? (d.rateLimitHit = !0, d.rateLimitError = t, [...e, {
|
|
125
|
+
publishedAt: null,
|
|
117
126
|
version: null,
|
|
118
|
-
actionName:
|
|
127
|
+
actionName: n,
|
|
119
128
|
sha: null
|
|
120
|
-
}]) : (console.warn(`Failed to check ${
|
|
129
|
+
}]) : (console.warn(`Failed to check ${n}:`, t), [...e, {
|
|
130
|
+
publishedAt: null,
|
|
121
131
|
version: null,
|
|
122
|
-
actionName:
|
|
132
|
+
actionName: n,
|
|
123
133
|
sha: null
|
|
124
134
|
}]);
|
|
125
135
|
}
|
|
126
136
|
}), Promise.resolve([]));
|
|
127
137
|
if (d.rateLimitError) {
|
|
128
|
-
let e = !!(
|
|
129
|
-
throw
|
|
138
|
+
let e = !!(i ?? process.env.GITHUB_TOKEN), t = `${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"}`, n = Error(t);
|
|
139
|
+
throw n.name = "GitHubRateLimitError", n;
|
|
130
140
|
}
|
|
131
141
|
let p = /* @__PURE__ */ new Map();
|
|
132
142
|
for (let e of f) p.set(e.actionName, {
|
|
143
|
+
publishedAt: e.publishedAt,
|
|
133
144
|
version: e.version,
|
|
134
145
|
sha: e.sha
|
|
135
146
|
});
|
|
136
147
|
let m = [];
|
|
137
148
|
for (let e of l) {
|
|
138
|
-
let
|
|
139
|
-
|
|
149
|
+
let t = p.get(e.name);
|
|
150
|
+
t ? m.push(createUpdate(e, {
|
|
151
|
+
publishedAt: t.publishedAt,
|
|
152
|
+
version: t.version,
|
|
153
|
+
sha: t.sha
|
|
154
|
+
})) : m.push(createUpdate(e, {
|
|
155
|
+
publishedAt: null,
|
|
156
|
+
version: null,
|
|
157
|
+
sha: null
|
|
158
|
+
}));
|
|
140
159
|
}
|
|
141
160
|
return m;
|
|
142
161
|
}
|
|
143
|
-
function createUpdate(e,
|
|
144
|
-
let s = normalizeVersion(e.version ?? ""),
|
|
145
|
-
if (
|
|
146
|
-
else if (
|
|
147
|
-
let
|
|
148
|
-
if (
|
|
149
|
-
if (
|
|
150
|
-
let e = semver.major(
|
|
151
|
-
|
|
162
|
+
function createUpdate(e, n) {
|
|
163
|
+
let { version: r, sha: s, publishedAt: c } = n, l = normalizeVersion(e.version ?? ""), u = r ? normalizeVersion(r) : null, d = !1, f = !1;
|
|
164
|
+
if (l && isSha(l)) s ? d = !compareSha(l, s) : u && (d = !0);
|
|
165
|
+
else if (l && u) {
|
|
166
|
+
let n = semver.valid(l), r = semver.valid(u);
|
|
167
|
+
if (n && r) {
|
|
168
|
+
if (d = semver.lt(n, r), d) {
|
|
169
|
+
let e = semver.major(n);
|
|
170
|
+
f = semver.major(r) > e;
|
|
152
171
|
}
|
|
153
|
-
!
|
|
154
|
-
} else
|
|
172
|
+
!d && semver.eq(n, r) && !isSha(e.version) && s && (d = !0, f = !1);
|
|
173
|
+
} else l !== u && (d = !0);
|
|
155
174
|
}
|
|
156
175
|
return {
|
|
157
176
|
currentVersion: e.version ?? "unknown",
|
|
158
177
|
latestVersion: r,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
publishedAt: c,
|
|
179
|
+
isBreaking: f,
|
|
180
|
+
latestSha: s,
|
|
181
|
+
hasUpdate: d,
|
|
162
182
|
action: e
|
|
163
183
|
};
|
|
164
184
|
}
|
|
165
|
-
function compareSha(e,
|
|
166
|
-
let
|
|
167
|
-
return
|
|
185
|
+
function compareSha(e, t) {
|
|
186
|
+
let n = e.replace(/^v/u, ""), r = t.replace(/^v/u, ""), i = Math.min(n.length, r.length);
|
|
187
|
+
return i < 7 ? !1 : n.slice(0, Math.max(0, i)).toLowerCase() === r.slice(0, Math.max(0, i)).toLowerCase();
|
|
168
188
|
}
|
|
169
189
|
function normalizeVersion(e) {
|
|
170
190
|
if (!e) return null;
|
|
171
|
-
let
|
|
172
|
-
if (/^[0-9a-f]{7,40}$/iu.test(
|
|
173
|
-
let
|
|
174
|
-
return
|
|
191
|
+
let n = e.replace(/^v/u, "");
|
|
192
|
+
if (/^[0-9a-f]{7,40}$/iu.test(n)) return e;
|
|
193
|
+
let r = semver.coerce(n);
|
|
194
|
+
return r ? r.version : e;
|
|
175
195
|
}
|
|
176
196
|
function isSha(e) {
|
|
177
197
|
if (!e) return !1;
|
|
178
|
-
let
|
|
179
|
-
return /^[0-9a-f]{7,40}$/iu.test(
|
|
198
|
+
let t = e.replace(/^v/u, "");
|
|
199
|
+
return /^[0-9a-f]{7,40}$/iu.test(t);
|
|
180
200
|
}
|
|
181
201
|
function isSemverLike(e) {
|
|
182
202
|
if (!e) return !1;
|
|
183
|
-
let
|
|
184
|
-
return /^v?\d+(?:\.\d+){0,2}$/u.test(
|
|
203
|
+
let t = e.trim();
|
|
204
|
+
return /^v?\d+(?:\.\d+){0,2}$/u.test(t);
|
|
185
205
|
}
|
|
186
206
|
export { checkUpdates };
|
|
@@ -11,6 +11,10 @@ function scanCompositeActionAst(a, o, s) {
|
|
|
11
11
|
let u = c.runs;
|
|
12
12
|
if (!u || !isCompositeActionRuns(u) || !u.steps || !Array.isArray(u.steps)) return [];
|
|
13
13
|
let d = findMapPair(l.value, "steps");
|
|
14
|
-
return d?.value ? extractUsesFromSteps(
|
|
14
|
+
return d?.value ? extractUsesFromSteps({
|
|
15
|
+
stepsNode: d.value,
|
|
16
|
+
filePath: s,
|
|
17
|
+
content: o
|
|
18
|
+
}) : [];
|
|
15
19
|
}
|
|
16
20
|
export { scanCompositeActionAst };
|
|
@@ -4,7 +4,8 @@ import { GitHubAction } from '../../../types/github-action';
|
|
|
4
4
|
* Scans a parsed workflow YAML document for action references.
|
|
5
5
|
*
|
|
6
6
|
* Navigates AST structure `jobs -> <job> -> steps` and extracts `uses` entries
|
|
7
|
-
* with corresponding line numbers.
|
|
7
|
+
* with corresponding line numbers. Also scans for job-level `uses` fields that
|
|
8
|
+
* indicate Reusable Workflows.
|
|
8
9
|
*
|
|
9
10
|
* @param document - Parsed YAML document of a workflow file.
|
|
10
11
|
* @param content - Original file content.
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { isWorkflowStructure } from "../../schema/workflow/is-workflow-structure.js";
|
|
2
|
+
import { parseActionReference } from "../../parsing/parse-action-reference.js";
|
|
3
|
+
import { getLineNumberForKey } from "../utils/get-line-number.js";
|
|
2
4
|
import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
5
|
+
import { isScalar } from "../guards/is-scalar.js";
|
|
3
6
|
import { isNode } from "../guards/is-node.js";
|
|
4
7
|
import { isPair } from "../guards/is-pair.js";
|
|
5
8
|
import { extractUsesFromSteps } from "../utils/extract-uses-from-steps.js";
|
|
6
9
|
import { findMapPair } from "../utils/find-map-pair.js";
|
|
7
|
-
function scanWorkflowAst(
|
|
8
|
-
if (!isWorkflowStructure(
|
|
9
|
-
let
|
|
10
|
-
if (!
|
|
11
|
-
let
|
|
12
|
-
for (let e of
|
|
10
|
+
function scanWorkflowAst(l, u, d) {
|
|
11
|
+
if (!isWorkflowStructure(l.toJSON()) || !l.contents || !isYAMLMap(l.contents)) return [];
|
|
12
|
+
let f = findMapPair(l.contents, "jobs");
|
|
13
|
+
if (!f?.value || !isYAMLMap(f.value)) return [];
|
|
14
|
+
let p = [];
|
|
15
|
+
for (let e of f.value.items) {
|
|
13
16
|
if (!isPair(e) || !e.value || !isNode(e.value) || !isYAMLMap(e.value)) continue;
|
|
14
|
-
let
|
|
15
|
-
|
|
17
|
+
let l = isScalar(e.key) ? String(e.key.value) : void 0, f = findMapPair(e.value, "uses");
|
|
18
|
+
if (f?.value && f.key && isScalar(f.value)) {
|
|
19
|
+
let e = parseActionReference(String(f.value.value), d, getLineNumberForKey(u, f.key));
|
|
20
|
+
e && (l && (e.job = l), p.push(e));
|
|
21
|
+
}
|
|
22
|
+
let m = findMapPair(e.value, "steps");
|
|
23
|
+
m?.value && p.push(...extractUsesFromSteps({
|
|
24
|
+
stepsNode: m.value,
|
|
25
|
+
filePath: d,
|
|
26
|
+
content: u,
|
|
27
|
+
jobName: l
|
|
28
|
+
}));
|
|
16
29
|
}
|
|
17
|
-
return
|
|
30
|
+
return p;
|
|
18
31
|
}
|
|
19
32
|
export { scanWorkflowAst };
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { GitHubAction } from '../../../types/github-action';
|
|
2
|
+
interface ExtractUsesOptions {
|
|
3
|
+
/** YAML sequence node containing workflow/action steps. */
|
|
4
|
+
stepsNode: unknown;
|
|
5
|
+
/** Path of the file being scanned (for metadata). */
|
|
6
|
+
filePath: string;
|
|
7
|
+
/** Name of the job containing these steps (for workflows). */
|
|
8
|
+
jobName?: string;
|
|
9
|
+
/** Original YAML file content (for line number calculation). */
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Extracts GitHub Action references from a steps YAML sequence.
|
|
4
14
|
*
|
|
5
15
|
* Uses the AST to locate the 'uses' key for precise line numbers and the JSON
|
|
6
16
|
* representation to validate the presence and type of the 'uses' field.
|
|
7
17
|
*
|
|
8
|
-
* @param
|
|
9
|
-
* @param filePath - Path of the file being scanned (for metadata).
|
|
10
|
-
* @param content - Original YAML file content (for line number calculation).
|
|
18
|
+
* @param options - Options for extraction.
|
|
11
19
|
* @returns List of discovered GitHub actions.
|
|
12
20
|
*/
|
|
13
|
-
export declare function extractUsesFromSteps(
|
|
21
|
+
export declare function extractUsesFromSteps(options: ExtractUsesOptions): GitHubAction[];
|
|
22
|
+
export {};
|
|
@@ -5,18 +5,19 @@ import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
|
5
5
|
import { isScalar } from "../guards/is-scalar.js";
|
|
6
6
|
import { isNode } from "../guards/is-node.js";
|
|
7
7
|
import { isPair } from "../guards/is-pair.js";
|
|
8
|
-
function extractUsesFromSteps(s
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
function extractUsesFromSteps(s) {
|
|
9
|
+
let { stepsNode: c, filePath: l, content: u, jobName: d } = s;
|
|
10
|
+
if (!isYAMLSequence(c)) return [];
|
|
11
|
+
let f = [];
|
|
12
|
+
for (let o of c.items) {
|
|
12
13
|
if (!isYAMLMap(o) || !isNode(o)) continue;
|
|
13
14
|
let s = o.toJSON();
|
|
14
15
|
if (typeof s != "object" || !s || Array.isArray(s)) continue;
|
|
15
|
-
let
|
|
16
|
-
if (typeof
|
|
17
|
-
let
|
|
18
|
-
|
|
16
|
+
let c = s;
|
|
17
|
+
if (typeof c.uses != "string") continue;
|
|
18
|
+
let p = o.items.find((e) => isPair(e) && isScalar(e.key) && e.key.value === "uses"), m = p?.key ? getLineNumberForKey(u, p.key) : 0, h = parseActionReference(c.uses, l, m);
|
|
19
|
+
h && (d && (h.job = d), f.push(h));
|
|
19
20
|
}
|
|
20
|
-
return
|
|
21
|
+
return f;
|
|
21
22
|
}
|
|
22
23
|
export { extractUsesFromSteps };
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { ActionUpdate } from '../../types/action-update';
|
|
2
|
-
|
|
2
|
+
interface PromptUpdateSelectionOptions {
|
|
3
|
+
/** Whether to show the Age column. */
|
|
4
|
+
showAge?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function promptUpdateSelection(updates: ActionUpdate[], options?: PromptUpdateSelectionOptions): Promise<ActionUpdate[] | null>;
|
|
7
|
+
export {};
|
|
@@ -7,97 +7,118 @@ import pc from "picocolors";
|
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import enquirer from "enquirer";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
var MIN_ACTION_WIDTH =
|
|
11
|
-
async function promptUpdateSelection(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
s
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
var MIN_ACTION_WIDTH = 40, MIN_JOB_WIDTH = 4, MIN_CURRENT_WIDTH = 16, MAX_VERSION_WIDTH = 7;
|
|
11
|
+
async function promptUpdateSelection(l, g = {}) {
|
|
12
|
+
let { showAge: y = !1 } = g;
|
|
13
|
+
if (l.length === 0) return null;
|
|
14
|
+
let b = l.filter((e) => e.hasUpdate);
|
|
15
|
+
if (b.length === 0) return console.info(pc.green("ā All actions are up to date!")), null;
|
|
16
|
+
let x = /* @__PURE__ */ new Map();
|
|
17
|
+
for (let [e, o] of b.entries()) {
|
|
18
|
+
let s = o.action.file ?? "unknown file", c = path.relative(path.join(process.cwd(), GITHUB_DIRECTORY), s);
|
|
19
|
+
c === "" && (c = s);
|
|
20
|
+
let l = x.get(c) ?? [];
|
|
21
|
+
l.push({
|
|
22
|
+
update: o,
|
|
22
23
|
index: e
|
|
23
|
-
}),
|
|
24
|
+
}), x.set(c, l);
|
|
24
25
|
}
|
|
25
|
-
let
|
|
26
|
-
let
|
|
26
|
+
let S = await Promise.all(b.map(async (e) => {
|
|
27
|
+
let a = formatVersionOrSha(e.currentVersion), o = e.currentVersion ?? void 0, s = null, c = null;
|
|
27
28
|
if (!e.currentVersion || !isSha(e.currentVersion)) return {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
versionForPadding: s,
|
|
30
|
+
effectiveForDiff: o,
|
|
31
|
+
shortSha: c,
|
|
32
|
+
display: a
|
|
30
33
|
};
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
effectiveForDiff: a,
|
|
38
|
-
display: i
|
|
34
|
+
let l = await tryReadInlineVersionComment(e.action.file, e.action.line);
|
|
35
|
+
return l && (c = e.currentVersion.slice(0, 7), s = formatVersionOrSha(l), a = s, o = l), {
|
|
36
|
+
versionForPadding: s,
|
|
37
|
+
effectiveForDiff: o,
|
|
38
|
+
shortSha: c,
|
|
39
|
+
display: a
|
|
39
40
|
};
|
|
40
|
-
})),
|
|
41
|
-
for (let [
|
|
42
|
-
let
|
|
43
|
-
|
|
41
|
+
})), C = [], w = stripAnsi("Action").length, T = stripAnsi("Current").length, E = stripAnsi("Job").length, D = 0, O = !1;
|
|
42
|
+
for (let [a, s] of b.entries()) {
|
|
43
|
+
let c = s.action.name, l = S[a].display, u = s.action.job ?? "ā";
|
|
44
|
+
if (w = Math.max(w, c.length), T = Math.max(T, stripAnsi(l).length), E = Math.max(E, u.length), s.latestVersion) {
|
|
45
|
+
let c = formatVersion(s.latestVersion, S[a]?.effectiveForDiff ?? s.currentVersion);
|
|
46
|
+
D = Math.max(D, stripAnsi(c).length);
|
|
47
|
+
}
|
|
48
|
+
let d = S[a]?.versionForPadding;
|
|
49
|
+
d && (D = Math.max(D, stripAnsi(d).length)), s.publishedAt && (O = !0);
|
|
44
50
|
}
|
|
45
|
-
let
|
|
46
|
-
for (let [
|
|
47
|
-
let
|
|
48
|
-
if (!
|
|
49
|
-
console.warn(`Unexpected missing group for file: ${
|
|
51
|
+
let k = Math.max(w, MIN_ACTION_WIDTH), A = Math.max(T, MIN_CURRENT_WIDTH), j = Math.max(E, MIN_JOB_WIDTH), M = Math.min(D, MAX_VERSION_WIDTH), N = M + 1 + 9, P = y && O ? 6 : 0, F = [...x.keys()].toSorted();
|
|
52
|
+
for (let [a, o] of F.entries()) {
|
|
53
|
+
let l = x.get(o);
|
|
54
|
+
if (!l) {
|
|
55
|
+
console.warn(`Unexpected missing group for file: ${o}`);
|
|
50
56
|
continue;
|
|
51
57
|
}
|
|
52
|
-
let
|
|
53
|
-
|
|
58
|
+
let u = [], d = l;
|
|
59
|
+
u.push({
|
|
54
60
|
current: "Current",
|
|
55
61
|
action: "Action",
|
|
56
62
|
target: "Target",
|
|
57
|
-
arrow: "āÆ"
|
|
63
|
+
arrow: "āÆ",
|
|
64
|
+
job: "Job",
|
|
65
|
+
age: "Age"
|
|
58
66
|
});
|
|
59
|
-
for (let { update:
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
for (let { update: a, index: o } of d) {
|
|
68
|
+
let l = !!a.latestSha, d = S[o], f = d.display;
|
|
69
|
+
d.versionForPadding && d.shortSha && (f = `${padString(d.versionForPadding, M + 1)}${pc.gray(`(${d.shortSha})`)}`);
|
|
70
|
+
let p = d.effectiveForDiff ?? a.currentVersion, m = formatVersion(a.latestVersion, p), h = a.action.name;
|
|
71
|
+
if (a.latestSha) {
|
|
72
|
+
let e = a.latestSha.slice(0, 7);
|
|
73
|
+
m = `${padString(m, M + 1)}${pc.gray(`(${e})`)}`;
|
|
64
74
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
l || (m = pc.gray(m), f = pc.gray(f), h = pc.gray(h));
|
|
76
|
+
let g = a.action.job ?? "ā", _ = formatAge(a.publishedAt);
|
|
77
|
+
u.push({
|
|
78
|
+
job: l ? g : pc.gray(g),
|
|
79
|
+
age: l ? _ : pc.gray(_),
|
|
80
|
+
action: h,
|
|
81
|
+
target: m,
|
|
68
82
|
arrow: "āÆ",
|
|
69
|
-
current:
|
|
83
|
+
current: f
|
|
70
84
|
});
|
|
71
85
|
}
|
|
72
|
-
let
|
|
73
|
-
for (let [e,
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
let h = Math.max(k, MIN_ACTION_WIDTH), g = Math.max(A, MIN_CURRENT_WIDTH), _ = Math.max(j, MIN_JOB_WIDTH), v = [];
|
|
87
|
+
for (let [e, a] of u.entries()) {
|
|
88
|
+
let o = e === 0, s = formatTableRow({
|
|
89
|
+
targetWidth: N,
|
|
90
|
+
currentWidth: g,
|
|
91
|
+
actionWidth: h,
|
|
92
|
+
ageWidth: P,
|
|
93
|
+
jobWidth: _,
|
|
94
|
+
row: a
|
|
95
|
+
});
|
|
96
|
+
if (o) v.push({
|
|
97
|
+
message: pc.gray(` ā ${s}`),
|
|
77
98
|
role: "separator",
|
|
78
99
|
indent: "",
|
|
79
100
|
name: ""
|
|
80
101
|
});
|
|
81
102
|
else {
|
|
82
|
-
let { update:
|
|
83
|
-
|
|
84
|
-
message:
|
|
85
|
-
value: String(
|
|
86
|
-
name: String(
|
|
87
|
-
disabled: !
|
|
103
|
+
let { update: a, index: o } = d[e - 1], c = !!a.latestSha, l = c && !a.isBreaking;
|
|
104
|
+
v.push({
|
|
105
|
+
message: s,
|
|
106
|
+
value: String(o),
|
|
107
|
+
name: String(o),
|
|
108
|
+
disabled: !c,
|
|
88
109
|
indent: "",
|
|
89
|
-
enabled:
|
|
110
|
+
enabled: l
|
|
90
111
|
});
|
|
91
112
|
}
|
|
92
113
|
}
|
|
93
|
-
|
|
94
|
-
message: pc.gray(
|
|
95
|
-
value: `label|${
|
|
96
|
-
choices:
|
|
97
|
-
name: `label|${
|
|
114
|
+
C.push({
|
|
115
|
+
message: pc.gray(o),
|
|
116
|
+
value: `label|${o}`,
|
|
117
|
+
choices: v,
|
|
118
|
+
name: `label|${o}`,
|
|
98
119
|
isGroupLabel: !0,
|
|
99
120
|
enabled: !1
|
|
100
|
-
}),
|
|
121
|
+
}), a < F.length - 1 && C.push({
|
|
101
122
|
role: "separator",
|
|
102
123
|
message: " ",
|
|
103
124
|
name: ""
|
|
@@ -105,12 +126,12 @@ async function promptUpdateSelection(o) {
|
|
|
105
126
|
}
|
|
106
127
|
try {
|
|
107
128
|
let e = {
|
|
108
|
-
indicator(e,
|
|
109
|
-
if (
|
|
110
|
-
let e = (
|
|
111
|
-
return ` ${pc.gray(
|
|
129
|
+
indicator(e, a) {
|
|
130
|
+
if (a.isGroupLabel) {
|
|
131
|
+
let e = (a.choices ?? []).filter((e) => !("role" in e)), o = e.length, s = e.filter((e) => !!e.enabled).length === o ? "ā" : "ā";
|
|
132
|
+
return ` ${pc.gray(s)}`;
|
|
112
133
|
}
|
|
113
|
-
return ` ${
|
|
134
|
+
return ` ${a.enabled ? "ā" : "ā"}`;
|
|
114
135
|
},
|
|
115
136
|
message: `Choose which actions to update (Press ${pc.cyan("<space>")} to select, ${pc.cyan("<a>")} to toggle all, ${pc.cyan("<i>")} to invert selection)`,
|
|
116
137
|
styles: {
|
|
@@ -131,46 +152,53 @@ async function promptUpdateSelection(o) {
|
|
|
131
152
|
type: "multiselect",
|
|
132
153
|
name: "selected",
|
|
133
154
|
pointer: "āÆ",
|
|
134
|
-
choices:
|
|
135
|
-
}, { selected:
|
|
136
|
-
for (let e of
|
|
155
|
+
choices: C
|
|
156
|
+
}, { selected: a } = await enquirer.prompt(e), o = /* @__PURE__ */ new Set();
|
|
157
|
+
for (let e of a) {
|
|
137
158
|
if (e.startsWith("label|")) {
|
|
138
|
-
let
|
|
139
|
-
for (let { update: e, index:
|
|
159
|
+
let a = e.slice(6), s = x.get(a) ?? [];
|
|
160
|
+
for (let { update: e, index: a } of s) e.latestSha && o.add(a);
|
|
140
161
|
continue;
|
|
141
162
|
}
|
|
142
|
-
let
|
|
143
|
-
Number.isFinite(
|
|
163
|
+
let a = Number.parseInt(e, 10);
|
|
164
|
+
Number.isFinite(a) && o.add(a);
|
|
144
165
|
}
|
|
145
|
-
let
|
|
146
|
-
for (let [e,
|
|
147
|
-
return
|
|
166
|
+
let s = [];
|
|
167
|
+
for (let [e, a] of b.entries()) o.has(e) && a.latestSha && s.push(a);
|
|
168
|
+
return s.length === 0 ? (console.info(pc.yellow("\nNo actions selected")), null) : s;
|
|
148
169
|
} catch (e) {
|
|
149
170
|
if (e instanceof Error && (e.message.includes("cancelled") || e.message.includes("ESC") || e.name === "ExitPromptError")) return logSelectionCancelled(), null;
|
|
150
171
|
throw console.error(pc.red("Unexpected error during selection:"), e), e;
|
|
151
172
|
}
|
|
152
173
|
}
|
|
153
|
-
async function tryReadInlineVersionComment(e,
|
|
174
|
+
async function tryReadInlineVersionComment(e, a) {
|
|
154
175
|
try {
|
|
155
|
-
if (!e || !
|
|
156
|
-
let
|
|
157
|
-
if (
|
|
158
|
-
let
|
|
159
|
-
if (
|
|
176
|
+
if (!e || !a || a <= 0) return null;
|
|
177
|
+
let o = (await readFile(e, "utf8")).split("\n"), s = a - 1;
|
|
178
|
+
if (s < 0 || s >= o.length) return null;
|
|
179
|
+
let c = o[s].match(/#\s*(?<version>[Vv]?\d+(?:\.\d+){0,2}(?:[+-][\w\-.]+)?)/u);
|
|
180
|
+
if (c?.groups?.version) return c.groups.version;
|
|
160
181
|
} catch {}
|
|
161
182
|
return null;
|
|
162
183
|
}
|
|
163
|
-
function
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
function formatAge(e) {
|
|
185
|
+
if (!e) return "";
|
|
186
|
+
let a = Date.now() - e.getTime(), o = Math.floor(a / (1e3 * 60 * 60)), s = Math.floor(o / 24), c = Math.floor(s / 7), l = s % 7;
|
|
187
|
+
return c >= 1 ? l > 0 ? `${c}w ${l}d` : `${c}w` : s >= 1 ? `${s}d` : `${o}h`;
|
|
188
|
+
}
|
|
189
|
+
function formatTableRow(e) {
|
|
190
|
+
let { currentWidth: a, actionWidth: o, targetWidth: c, jobWidth: l, ageWidth: u, row: d } = e, f = [
|
|
191
|
+
padString(d.action, o),
|
|
192
|
+
padString(d.job, l),
|
|
193
|
+
padString(d.current, a),
|
|
194
|
+
d.arrow,
|
|
195
|
+
padString(d.target, c)
|
|
196
|
+
];
|
|
197
|
+
return u > 0 && f.push(d.age), f.join(" ").replace(/\s+$/u, "");
|
|
170
198
|
}
|
|
171
199
|
function isSha(e) {
|
|
172
|
-
let
|
|
173
|
-
return /^[0-9a-f]{7,40}$/iu.test(
|
|
200
|
+
let a = e.replace(/^v/u, "");
|
|
201
|
+
return /^[0-9a-f]{7,40}$/iu.test(a);
|
|
174
202
|
}
|
|
175
203
|
function formatVersionOrSha(e) {
|
|
176
204
|
return e ? isSha(e) ? e.slice(0, 7) : e.replace(/^v/u, "") : pc.gray("unknown");
|
|
@@ -24,7 +24,7 @@ function parseActionReference(e, t, n) {
|
|
|
24
24
|
if (!s || !c) return null;
|
|
25
25
|
for (let e of o.slice(2)) if (!e) return null;
|
|
26
26
|
return {
|
|
27
|
-
type: "external",
|
|
27
|
+
type: o.length > 2 && (i.endsWith(".yml") || i.endsWith(".yaml")) ? "reusable-workflow" : "external",
|
|
28
28
|
name: i,
|
|
29
29
|
version: a,
|
|
30
30
|
file: t,
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "1.
|
|
1
|
+
const version = "1.6.0";
|
|
2
2
|
export { version };
|
|
@@ -10,6 +10,9 @@ export interface ActionUpdate {
|
|
|
10
10
|
/** SHA hash of the latest version. */
|
|
11
11
|
latestSha: string | null
|
|
12
12
|
|
|
13
|
+
/** Publication date of the latest version (null if unknown). */
|
|
14
|
+
publishedAt: Date | null
|
|
15
|
+
|
|
13
16
|
/** The original action from scanning. */
|
|
14
17
|
action: GitHubAction
|
|
15
18
|
|
|
@@ -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
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
|
|
@@ -379,6 +380,8 @@ jobs:
|
|
|
379
380
|
|
|
380
381
|
## Example
|
|
381
382
|
|
|
383
|
+
### Regular Actions
|
|
384
|
+
|
|
382
385
|
```yaml
|
|
383
386
|
# Before
|
|
384
387
|
- uses: actions/checkout@v3
|
|
@@ -389,6 +392,26 @@ jobs:
|
|
|
389
392
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
390
393
|
```
|
|
391
394
|
|
|
395
|
+
### Reusable Workflows
|
|
396
|
+
|
|
397
|
+
Actions Up also detects and updates reusable workflow calls:
|
|
398
|
+
|
|
399
|
+
```yaml
|
|
400
|
+
# Before
|
|
401
|
+
jobs:
|
|
402
|
+
call-workflow:
|
|
403
|
+
uses: org/repo/.github/workflows/ci.yml@v1.0.0
|
|
404
|
+
with:
|
|
405
|
+
config: production
|
|
406
|
+
|
|
407
|
+
# After running actions-up
|
|
408
|
+
jobs:
|
|
409
|
+
call-workflow:
|
|
410
|
+
uses: org/repo/.github/workflows/ci.yml@a1b2c3d4e5f6 # v2.0.0
|
|
411
|
+
with:
|
|
412
|
+
config: production
|
|
413
|
+
```
|
|
414
|
+
|
|
392
415
|
## Advanced Usage
|
|
393
416
|
|
|
394
417
|
### Using GitHub Token for Higher Rate Limits
|
|
@@ -439,6 +462,17 @@ npx actions-up --exclude ".*/internal-.*" --exclude "/^acme\/.+$/i"
|
|
|
439
462
|
npx actions-up --exclude "my-org/.*, .*/internal-.*"
|
|
440
463
|
```
|
|
441
464
|
|
|
465
|
+
#### Filtering by Release Age
|
|
466
|
+
|
|
467
|
+
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:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
# Only show updates released at least 7 days ago
|
|
471
|
+
npx actions-up --min-age 7
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
When `--min-age` is set, an "Age" column appears showing how long ago each release was published (e.g., `3d`, `1w 2d`).
|
|
475
|
+
|
|
442
476
|
#### Ignore Comments
|
|
443
477
|
|
|
444
478
|
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`.
|