actions-up 1.4.2 ā 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 +7 -5
- package/dist/core/api/check-updates.js +107 -88
- 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/update/apply-updates.js +1 -1
- 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 +121 -96
- package/dist/core/parsing/parse-action-reference.js +1 -1
- package/dist/core/scan-github-actions.d.ts +3 -1
- package/dist/core/scan-github-actions.js +39 -50
- 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 +42 -0
package/dist/cli/index.js
CHANGED
|
@@ -11,11 +11,11 @@ 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("--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 {
|
|
18
|
-
let l = await scanGitHubActions(process.cwd()), u = l.actions.length, d = l.workflows.size, f = l.compositeActions.size;
|
|
18
|
+
let l = await scanGitHubActions(process.cwd(), s.dir), u = l.actions.length, d = l.workflows.size, f = l.compositeActions.size;
|
|
19
19
|
if (c.success(`Found ${pc.yellow(u)} actions in ${pc.yellow(d)} workflows and ${pc.yellow(f)} composite actions`), u === 0) {
|
|
20
20
|
console.info(pc.green("\n⨠No GitHub Actions found in this repository"));
|
|
21
21
|
return;
|
|
@@ -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,187 +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
|
-
|
|
167
|
-
|
|
168
|
-
return a < 7 ? !1 : r.slice(0, Math.max(0, a)).toLowerCase() === i.slice(0, Math.max(0, a)).toLowerCase();
|
|
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();
|
|
169
188
|
}
|
|
170
189
|
function normalizeVersion(e) {
|
|
171
190
|
if (!e) return null;
|
|
172
|
-
let
|
|
173
|
-
if (/^[0-9a-f]{7,40}$/iu.test(
|
|
174
|
-
let
|
|
175
|
-
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;
|
|
176
195
|
}
|
|
177
196
|
function isSha(e) {
|
|
178
197
|
if (!e) return !1;
|
|
179
|
-
let
|
|
180
|
-
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);
|
|
181
200
|
}
|
|
182
201
|
function isSemverLike(e) {
|
|
183
202
|
if (!e) return !1;
|
|
184
|
-
let
|
|
185
|
-
return /^v?\d+(?:\.\d+){0,2}$/u.test(
|
|
203
|
+
let t = e.trim();
|
|
204
|
+
return /^v?\d+(?:\.\d+){0,2}$/u.test(t);
|
|
186
205
|
}
|
|
187
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 };
|
|
@@ -27,7 +27,7 @@ async function applyUpdates(n) {
|
|
|
27
27
|
console.error(`Invalid SHA format: ${e.latestSha}`);
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
|
-
let a = r ? String.raw`(?=[^\S\r\n]|$|#)` : "", o = RegExp(`(
|
|
30
|
+
let a = r ? String.raw`(?=[^\S\r\n]|$|#)` : "", o = new RegExp(String.raw`(^\s*-?\s*uses:\s*)(['"]?)(${n})@${r}\2${a}([^\S\r\n]*#[^\r\n]*)?`, "gm"), s = `$1$2$3@${e.latestSha}$2 # ${e.latestVersion}`;
|
|
31
31
|
i = i.replace(o, s);
|
|
32
32
|
}
|
|
33
33
|
await writeFile(n, i, "utf8");
|
|
@@ -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,99 +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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
value: String(s),
|
|
88
|
-
name: String(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),
|
|
89
108
|
disabled: !c,
|
|
90
109
|
indent: "",
|
|
91
|
-
enabled:
|
|
110
|
+
enabled: l
|
|
92
111
|
});
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
|
-
|
|
96
|
-
message: pc.gray(
|
|
97
|
-
value: `label|${
|
|
98
|
-
choices:
|
|
99
|
-
name: `label|${
|
|
114
|
+
C.push({
|
|
115
|
+
message: pc.gray(o),
|
|
116
|
+
value: `label|${o}`,
|
|
117
|
+
choices: v,
|
|
118
|
+
name: `label|${o}`,
|
|
100
119
|
isGroupLabel: !0,
|
|
101
120
|
enabled: !1
|
|
102
|
-
}),
|
|
121
|
+
}), a < F.length - 1 && C.push({
|
|
103
122
|
role: "separator",
|
|
104
123
|
message: " ",
|
|
105
124
|
name: ""
|
|
@@ -107,12 +126,12 @@ async function promptUpdateSelection(o) {
|
|
|
107
126
|
}
|
|
108
127
|
try {
|
|
109
128
|
let e = {
|
|
110
|
-
indicator(e,
|
|
111
|
-
if (
|
|
112
|
-
let e = (
|
|
113
|
-
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)}`;
|
|
114
133
|
}
|
|
115
|
-
return ` ${
|
|
134
|
+
return ` ${a.enabled ? "ā" : "ā"}`;
|
|
116
135
|
},
|
|
117
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)`,
|
|
118
137
|
styles: {
|
|
@@ -133,47 +152,53 @@ async function promptUpdateSelection(o) {
|
|
|
133
152
|
type: "multiselect",
|
|
134
153
|
name: "selected",
|
|
135
154
|
pointer: "āÆ",
|
|
136
|
-
choices:
|
|
137
|
-
}, { selected:
|
|
138
|
-
for (let e of
|
|
155
|
+
choices: C
|
|
156
|
+
}, { selected: a } = await enquirer.prompt(e), o = /* @__PURE__ */ new Set();
|
|
157
|
+
for (let e of a) {
|
|
139
158
|
if (e.startsWith("label|")) {
|
|
140
|
-
let
|
|
141
|
-
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);
|
|
142
161
|
continue;
|
|
143
162
|
}
|
|
144
|
-
let
|
|
145
|
-
Number.isFinite(
|
|
163
|
+
let a = Number.parseInt(e, 10);
|
|
164
|
+
Number.isFinite(a) && o.add(a);
|
|
146
165
|
}
|
|
147
|
-
let
|
|
148
|
-
for (let [e,
|
|
149
|
-
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;
|
|
150
169
|
} catch (e) {
|
|
151
170
|
if (e instanceof Error && (e.message.includes("cancelled") || e.message.includes("ESC") || e.name === "ExitPromptError")) return logSelectionCancelled(), null;
|
|
152
171
|
throw console.error(pc.red("Unexpected error during selection:"), e), e;
|
|
153
172
|
}
|
|
154
173
|
}
|
|
155
|
-
async function tryReadInlineVersionComment(e,
|
|
174
|
+
async function tryReadInlineVersionComment(e, a) {
|
|
156
175
|
try {
|
|
157
|
-
if (!e || !
|
|
158
|
-
let
|
|
159
|
-
if (
|
|
160
|
-
let
|
|
161
|
-
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;
|
|
162
181
|
} catch {}
|
|
163
182
|
return null;
|
|
164
183
|
}
|
|
165
|
-
function
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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, "");
|
|
172
198
|
}
|
|
173
199
|
function isSha(e) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
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);
|
|
177
202
|
}
|
|
178
203
|
function formatVersionOrSha(e) {
|
|
179
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,
|
|
@@ -8,10 +8,12 @@ import { ScanResult } from '../types/scan-result';
|
|
|
8
8
|
*
|
|
9
9
|
* @param rootPath - The root path of the repository to scan. Defaults to
|
|
10
10
|
* current working directory.
|
|
11
|
+
* @param ciDirectory - The CI directory name (e.g., '.github' or '.gitea').
|
|
12
|
+
* Defaults to '.github'.
|
|
11
13
|
* @returns A promise that resolves to a ScanResult containing:
|
|
12
14
|
*
|
|
13
15
|
* - Workflows: Map of workflow file paths to their referenced actions
|
|
14
16
|
* - CompositeActions: Map of composite action names to their directory paths
|
|
15
17
|
* - Actions: Flat array of all discovered GitHub Actions.
|
|
16
18
|
*/
|
|
17
|
-
export declare function scanGitHubActions(rootPath?: string): Promise<ScanResult>;
|
|
19
|
+
export declare function scanGitHubActions(rootPath?: string, ciDirectory?: string): Promise<ScanResult>;
|
|
@@ -4,96 +4,85 @@ import { scanActionFile } from "./scan-action-file.js";
|
|
|
4
4
|
import { isYamlFile } from "./fs/is-yaml-file.js";
|
|
5
5
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
6
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
-
async function scanGitHubActions(d = process.cwd()) {
|
|
8
|
-
let
|
|
7
|
+
async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
8
|
+
let h = {
|
|
9
9
|
compositeActions: /* @__PURE__ */ new Map(),
|
|
10
10
|
workflows: /* @__PURE__ */ new Map(),
|
|
11
11
|
actions: []
|
|
12
|
-
},
|
|
13
|
-
function
|
|
12
|
+
}, g = resolve(d);
|
|
13
|
+
function _(e, o) {
|
|
14
14
|
let s = relative(e, o);
|
|
15
15
|
return s !== "" && !s.startsWith("..") && !isAbsolute(s);
|
|
16
16
|
}
|
|
17
|
-
let
|
|
18
|
-
if (!g
|
|
19
|
-
function
|
|
17
|
+
let v = join(g, m);
|
|
18
|
+
if (!_(g, v)) throw Error("Invalid path: detected path traversal attempt");
|
|
19
|
+
function y(e) {
|
|
20
20
|
return e.includes("..") || e.includes("/") || e.includes("\\") ? (console.warn(`Skipping invalid name: ${e}`), !1) : !0;
|
|
21
21
|
}
|
|
22
|
-
let
|
|
23
|
-
if (!g(h, y)) return m;
|
|
22
|
+
let b = join(v, WORKFLOWS_DIRECTORY);
|
|
24
23
|
try {
|
|
25
|
-
if ((await stat(
|
|
26
|
-
let e = (await readdir(
|
|
27
|
-
let
|
|
28
|
-
if (!g(y, l)) return console.warn(`Skipping file outside workflows directory: ${e}`), {
|
|
29
|
-
success: !1,
|
|
30
|
-
actions: [],
|
|
31
|
-
path: ""
|
|
32
|
-
};
|
|
24
|
+
if ((await stat(b)).isDirectory()) {
|
|
25
|
+
let e = (await readdir(b)).filter((e) => y(e) ? isYamlFile(e) : !1).map(async (e) => {
|
|
26
|
+
let o = join(b, e);
|
|
33
27
|
try {
|
|
34
|
-
let
|
|
28
|
+
let l = await scanWorkflowFile(o);
|
|
35
29
|
return {
|
|
36
|
-
path: `${
|
|
30
|
+
path: `${m}/${WORKFLOWS_DIRECTORY}/${e}`,
|
|
37
31
|
success: !0,
|
|
38
|
-
actions:
|
|
32
|
+
actions: l
|
|
39
33
|
};
|
|
40
34
|
} catch {
|
|
41
35
|
return {
|
|
42
|
-
path: `${
|
|
36
|
+
path: `${m}/${WORKFLOWS_DIRECTORY}/${e}`,
|
|
43
37
|
success: !1,
|
|
44
38
|
actions: []
|
|
45
39
|
};
|
|
46
40
|
}
|
|
47
|
-
}),
|
|
48
|
-
for (let e of
|
|
41
|
+
}), o = await Promise.all(e);
|
|
42
|
+
for (let e of o) e.success && e.path && (e.actions.length > 0 ? (h.workflows.set(e.path, e.actions), h.actions.push(...e.actions)) : h.workflows.set(e.path, []));
|
|
49
43
|
}
|
|
50
44
|
} catch {}
|
|
51
|
-
let
|
|
52
|
-
if (!g(h, b)) return m;
|
|
45
|
+
let x = join(v, ACTIONS_DIRECTORY);
|
|
53
46
|
try {
|
|
54
|
-
if ((await stat(
|
|
55
|
-
let
|
|
56
|
-
if (!
|
|
57
|
-
let
|
|
58
|
-
if (!g(b, c)) return console.warn(`Skipping subdirectory outside actions path: ${s}`), null;
|
|
47
|
+
if ((await stat(x)).isDirectory()) {
|
|
48
|
+
let o = (await readdir(x)).map(async (o) => {
|
|
49
|
+
if (!y(o)) return null;
|
|
50
|
+
let s = join(x, o);
|
|
59
51
|
try {
|
|
60
|
-
if (!(await stat(
|
|
61
|
-
let
|
|
62
|
-
if (!g(c, u)) return null;
|
|
63
|
-
let d = [];
|
|
52
|
+
if (!(await stat(s)).isDirectory()) return null;
|
|
53
|
+
let c = join(s, "action.yml"), u = [];
|
|
64
54
|
try {
|
|
65
|
-
|
|
55
|
+
u = await scanActionFile(c);
|
|
66
56
|
} catch {
|
|
67
57
|
try {
|
|
68
|
-
|
|
69
|
-
d = await scanActionFile(u);
|
|
58
|
+
c = join(s, "action.yaml"), u = await scanActionFile(c);
|
|
70
59
|
} catch {
|
|
71
60
|
return null;
|
|
72
61
|
}
|
|
73
62
|
}
|
|
74
63
|
return {
|
|
75
|
-
path: `${
|
|
76
|
-
name:
|
|
77
|
-
actions:
|
|
64
|
+
path: `${m}/${ACTIONS_DIRECTORY}/${o}`,
|
|
65
|
+
name: o,
|
|
66
|
+
actions: u
|
|
78
67
|
};
|
|
79
68
|
} catch {
|
|
80
69
|
return null;
|
|
81
70
|
}
|
|
82
|
-
}),
|
|
83
|
-
for (let e of
|
|
71
|
+
}), s = await Promise.all(o);
|
|
72
|
+
for (let e of s) e && (h.compositeActions.set(e.name, e.path), h.actions.push(...e.actions));
|
|
84
73
|
}
|
|
85
74
|
} catch {}
|
|
86
75
|
try {
|
|
87
|
-
let e = await getCurrentRepoSlug(
|
|
76
|
+
let e = await getCurrentRepoSlug(g);
|
|
88
77
|
if (e) {
|
|
89
78
|
if (process.env.ACTIONS_UP_TEST_THROW === "1") throw Error("test");
|
|
90
79
|
let o = /* @__PURE__ */ new Set(), s = [];
|
|
91
|
-
for (let c of
|
|
80
|
+
for (let c of h.actions) {
|
|
92
81
|
if (c.type !== "external") continue;
|
|
93
82
|
let l = c.name.split("/");
|
|
94
83
|
if (l.length < 3 || `${l[0]}/${l[1]}` !== e) continue;
|
|
95
|
-
let u = join(
|
|
96
|
-
g
|
|
84
|
+
let u = join(g, ...l.slice(2));
|
|
85
|
+
_(g, u) && (o.has(u) || (o.add(u), s.push(u)));
|
|
97
86
|
}
|
|
98
87
|
async function c() {
|
|
99
88
|
if (s.length === 0) return;
|
|
@@ -107,14 +96,14 @@ async function scanGitHubActions(d = process.cwd()) {
|
|
|
107
96
|
d = u;
|
|
108
97
|
}
|
|
109
98
|
let f = await scanActionFile(d);
|
|
110
|
-
f.length > 0 &&
|
|
99
|
+
f.length > 0 && h.actions.push(...f);
|
|
111
100
|
let p = [];
|
|
112
101
|
for (let s of f) {
|
|
113
102
|
if (s.type !== "external") continue;
|
|
114
103
|
let c = s.name.split("/");
|
|
115
104
|
if (c.length < 3 || `${c[0]}/${c[1]}` !== e) continue;
|
|
116
|
-
let l = join(
|
|
117
|
-
g
|
|
105
|
+
let l = join(g, ...c.slice(2));
|
|
106
|
+
_(g, l) && (o.has(l) || (o.add(l), p.push(l)));
|
|
118
107
|
}
|
|
119
108
|
return p;
|
|
120
109
|
} catch {
|
|
@@ -127,7 +116,7 @@ async function scanGitHubActions(d = process.cwd()) {
|
|
|
127
116
|
await c();
|
|
128
117
|
}
|
|
129
118
|
} catch {}
|
|
130
|
-
return
|
|
119
|
+
return h;
|
|
131
120
|
}
|
|
132
121
|
async function getCurrentRepoSlug(e) {
|
|
133
122
|
let o = process.env.GITHUB_REPOSITORY;
|
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
|
|
@@ -122,6 +123,14 @@ Check for updates without making any changes:
|
|
|
122
123
|
npx actions-up --dry-run
|
|
123
124
|
```
|
|
124
125
|
|
|
126
|
+
### Custom Directory
|
|
127
|
+
|
|
128
|
+
By default, Actions Up scans the `.github` directory. You can specify a different directory (e.g., for Gitea):
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx actions-up --dir .gitea
|
|
132
|
+
```
|
|
133
|
+
|
|
125
134
|
## GitHub Actions Integration
|
|
126
135
|
|
|
127
136
|
### Automated PR Checks
|
|
@@ -371,6 +380,8 @@ jobs:
|
|
|
371
380
|
|
|
372
381
|
## Example
|
|
373
382
|
|
|
383
|
+
### Regular Actions
|
|
384
|
+
|
|
374
385
|
```yaml
|
|
375
386
|
# Before
|
|
376
387
|
- uses: actions/checkout@v3
|
|
@@ -381,6 +392,26 @@ jobs:
|
|
|
381
392
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
382
393
|
```
|
|
383
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
|
+
|
|
384
415
|
## Advanced Usage
|
|
385
416
|
|
|
386
417
|
### Using GitHub Token for Higher Rate Limits
|
|
@@ -431,6 +462,17 @@ npx actions-up --exclude ".*/internal-.*" --exclude "/^acme\/.+$/i"
|
|
|
431
462
|
npx actions-up --exclude "my-org/.*, .*/internal-.*"
|
|
432
463
|
```
|
|
433
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
|
+
|
|
434
476
|
#### Ignore Comments
|
|
435
477
|
|
|
436
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`.
|