actions-up 1.12.1 → 1.13.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/build-json-report.d.ts +257 -0
- package/dist/cli/build-json-report.js +64 -0
- package/dist/cli/index.js +132 -73
- package/dist/cli/validate-cli-options.d.ts +20 -0
- package/dist/cli/validate-cli-options.js +4 -0
- package/dist/core/fs/find-yaml-files-recursive.js +1 -1
- package/dist/core/interactive/prompt-update-selection.js +9 -9
- package/dist/core/scan-github-actions.js +67 -67
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/readme.md +45 -50
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { ActionUpdate } from '../types/action-update';
|
|
2
|
+
import { ScanResult } from '../types/scan-result';
|
|
3
|
+
import { UpdateMode } from '../types/update-mode';
|
|
4
|
+
/**
|
|
5
|
+
* High-level status of a JSON report.
|
|
6
|
+
*/
|
|
7
|
+
export type JsonReportStatus = 'updates-available' | 'no-actions-found' | 'nothing-to-check' | 'up-to-date';
|
|
8
|
+
/**
|
|
9
|
+
* Options used to build a JSON report from the current CLI state.
|
|
10
|
+
*/
|
|
11
|
+
interface BuildJsonReportOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Updates excluded by the selected update mode.
|
|
14
|
+
*/
|
|
15
|
+
blockedByMode: ActionUpdate[];
|
|
16
|
+
/**
|
|
17
|
+
* Number of actions that were actually checked after excludes.
|
|
18
|
+
*/
|
|
19
|
+
actionsToCheckCount: number;
|
|
20
|
+
/**
|
|
21
|
+
* Regex patterns supplied through `--exclude`.
|
|
22
|
+
*/
|
|
23
|
+
excludePatterns: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether branch references were included in update checks.
|
|
26
|
+
*/
|
|
27
|
+
includeBranches: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Updates that remain actionable after all filters.
|
|
30
|
+
*/
|
|
31
|
+
outdated: ActionUpdate[];
|
|
32
|
+
/**
|
|
33
|
+
* Final report status.
|
|
34
|
+
*/
|
|
35
|
+
status: JsonReportStatus;
|
|
36
|
+
/**
|
|
37
|
+
* Updates skipped during processing, such as branch references.
|
|
38
|
+
*/
|
|
39
|
+
skipped: ActionUpdate[];
|
|
40
|
+
/**
|
|
41
|
+
* Aggregate scan result for the current run.
|
|
42
|
+
*/
|
|
43
|
+
scanResult: ScanResult;
|
|
44
|
+
/**
|
|
45
|
+
* Directories resolved for scanning.
|
|
46
|
+
*/
|
|
47
|
+
directories: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Whether recursive scanning mode is enabled.
|
|
50
|
+
*/
|
|
51
|
+
recursive: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Effective update mode for the run.
|
|
54
|
+
*/
|
|
55
|
+
mode: UpdateMode;
|
|
56
|
+
/**
|
|
57
|
+
* Minimum age filter in days.
|
|
58
|
+
*/
|
|
59
|
+
minAge: number;
|
|
60
|
+
/**
|
|
61
|
+
* Working directory used for relative path normalization.
|
|
62
|
+
*/
|
|
63
|
+
cwd?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Serialized update entry in the JSON report.
|
|
67
|
+
*/
|
|
68
|
+
interface JsonReportUpdate {
|
|
69
|
+
/**
|
|
70
|
+
* Reason why this entry was skipped, if any.
|
|
71
|
+
*/
|
|
72
|
+
skipReason: ActionUpdate['skipReason'] | null;
|
|
73
|
+
/**
|
|
74
|
+
* Processing status for this entry.
|
|
75
|
+
*/
|
|
76
|
+
status: NonNullable<ActionUpdate['status']>;
|
|
77
|
+
/**
|
|
78
|
+
* Version currently used in the workflow or action file.
|
|
79
|
+
*/
|
|
80
|
+
currentVersion: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Latest version found for this dependency.
|
|
83
|
+
*/
|
|
84
|
+
latestVersion: string | null;
|
|
85
|
+
/**
|
|
86
|
+
* Release publication date in ISO format.
|
|
87
|
+
*/
|
|
88
|
+
publishedAt: string | null;
|
|
89
|
+
/**
|
|
90
|
+
* Original action reference metadata.
|
|
91
|
+
*/
|
|
92
|
+
action: JsonReportAction;
|
|
93
|
+
/**
|
|
94
|
+
* Resolved SHA for the target version.
|
|
95
|
+
*/
|
|
96
|
+
latestSha: string | null;
|
|
97
|
+
/**
|
|
98
|
+
* Whether this update crosses a major version boundary.
|
|
99
|
+
*/
|
|
100
|
+
isBreaking: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Whether an update is available for this entry.
|
|
103
|
+
*/
|
|
104
|
+
hasUpdate: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Aggregate counts included in the JSON report.
|
|
108
|
+
*/
|
|
109
|
+
interface JsonReportSummary {
|
|
110
|
+
/**
|
|
111
|
+
* Number of composite actions discovered during scanning.
|
|
112
|
+
*/
|
|
113
|
+
totalCompositeActions: number;
|
|
114
|
+
/**
|
|
115
|
+
* Number of breaking updates among actionable updates.
|
|
116
|
+
*/
|
|
117
|
+
totalBreakingUpdates: number;
|
|
118
|
+
/**
|
|
119
|
+
* Number of actions checked after excludes.
|
|
120
|
+
*/
|
|
121
|
+
totalActionsChecked: number;
|
|
122
|
+
/**
|
|
123
|
+
* Number of updates filtered out by `--mode`.
|
|
124
|
+
*/
|
|
125
|
+
totalBlockedByMode: number;
|
|
126
|
+
/**
|
|
127
|
+
* Number of workflows discovered during scanning.
|
|
128
|
+
*/
|
|
129
|
+
totalWorkflows: number;
|
|
130
|
+
/**
|
|
131
|
+
* Total number of action references found during scanning.
|
|
132
|
+
*/
|
|
133
|
+
totalActions: number;
|
|
134
|
+
/**
|
|
135
|
+
* Number of skipped entries in the report.
|
|
136
|
+
*/
|
|
137
|
+
totalSkipped: number;
|
|
138
|
+
/**
|
|
139
|
+
* Number of actionable updates in the report.
|
|
140
|
+
*/
|
|
141
|
+
totalUpdates: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Serialized action reference included in each update entry.
|
|
145
|
+
*/
|
|
146
|
+
interface JsonReportAction {
|
|
147
|
+
/**
|
|
148
|
+
* Action reference type detected during scanning.
|
|
149
|
+
*/
|
|
150
|
+
type: ActionUpdate['action']['type'];
|
|
151
|
+
/**
|
|
152
|
+
* Original version or ref from the file, if available.
|
|
153
|
+
*/
|
|
154
|
+
version: string | null;
|
|
155
|
+
/**
|
|
156
|
+
* Relative or absolute file path for the action reference.
|
|
157
|
+
*/
|
|
158
|
+
file: string | null;
|
|
159
|
+
/**
|
|
160
|
+
* Line number of the action reference.
|
|
161
|
+
*/
|
|
162
|
+
line: number | null;
|
|
163
|
+
/**
|
|
164
|
+
* Original `uses` value, if available.
|
|
165
|
+
*/
|
|
166
|
+
uses: string | null;
|
|
167
|
+
/**
|
|
168
|
+
* Workflow job name, when applicable.
|
|
169
|
+
*/
|
|
170
|
+
job: string | null;
|
|
171
|
+
/**
|
|
172
|
+
* Full `owner/repo@ref` string, when available.
|
|
173
|
+
*/
|
|
174
|
+
ref: string | null;
|
|
175
|
+
/**
|
|
176
|
+
* Normalized action name.
|
|
177
|
+
*/
|
|
178
|
+
name: string;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Effective CLI options serialized into the report.
|
|
182
|
+
*/
|
|
183
|
+
interface JsonReportOptions {
|
|
184
|
+
/**
|
|
185
|
+
* Regex patterns supplied through `--exclude`.
|
|
186
|
+
*/
|
|
187
|
+
excludePatterns: string[];
|
|
188
|
+
/**
|
|
189
|
+
* Whether branch references were checked.
|
|
190
|
+
*/
|
|
191
|
+
includeBranches: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Resolved scan directories.
|
|
194
|
+
*/
|
|
195
|
+
directories: string[];
|
|
196
|
+
/**
|
|
197
|
+
* Whether recursive scanning mode is enabled.
|
|
198
|
+
*/
|
|
199
|
+
recursive: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Effective update mode.
|
|
202
|
+
*/
|
|
203
|
+
mode: UpdateMode;
|
|
204
|
+
/**
|
|
205
|
+
* Indicates that JSON mode never applies changes.
|
|
206
|
+
*/
|
|
207
|
+
reportOnly: true;
|
|
208
|
+
/**
|
|
209
|
+
* Minimum age filter in days.
|
|
210
|
+
*/
|
|
211
|
+
minAge: number;
|
|
212
|
+
/**
|
|
213
|
+
* Indicates that this payload came from `--json`.
|
|
214
|
+
*/
|
|
215
|
+
json: true;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Top-level machine-readable report emitted by `--json`.
|
|
219
|
+
*/
|
|
220
|
+
interface JsonReport {
|
|
221
|
+
/**
|
|
222
|
+
* Entries filtered out by the selected update mode.
|
|
223
|
+
*/
|
|
224
|
+
blockedByMode: JsonReportUpdate[];
|
|
225
|
+
/**
|
|
226
|
+
* Entries skipped during update checks.
|
|
227
|
+
*/
|
|
228
|
+
skipped: JsonReportUpdate[];
|
|
229
|
+
/**
|
|
230
|
+
* Actionable updates after filtering.
|
|
231
|
+
*/
|
|
232
|
+
updates: JsonReportUpdate[];
|
|
233
|
+
/**
|
|
234
|
+
* Effective options that shaped the report.
|
|
235
|
+
*/
|
|
236
|
+
options: JsonReportOptions;
|
|
237
|
+
/**
|
|
238
|
+
* Aggregate counts for the current run.
|
|
239
|
+
*/
|
|
240
|
+
summary: JsonReportSummary;
|
|
241
|
+
/**
|
|
242
|
+
* Overall outcome for the current run.
|
|
243
|
+
*/
|
|
244
|
+
status: JsonReportStatus;
|
|
245
|
+
/**
|
|
246
|
+
* Version of the JSON payload schema.
|
|
247
|
+
*/
|
|
248
|
+
schemaVersion: 1;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Build the machine-readable JSON report returned by `--json`.
|
|
252
|
+
*
|
|
253
|
+
* @param options - Current CLI state and computed update data.
|
|
254
|
+
* @returns Serializable JSON payload for stdout.
|
|
255
|
+
*/
|
|
256
|
+
export declare function buildJsonReport(options: BuildJsonReportOptions): JsonReport;
|
|
257
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
function buildJsonReport(e) {
|
|
3
|
+
let n = resolve(e.cwd ?? process.cwd());
|
|
4
|
+
return {
|
|
5
|
+
summary: {
|
|
6
|
+
totalBreakingUpdates: e.outdated.filter((e) => e.isBreaking).length,
|
|
7
|
+
totalCompositeActions: e.scanResult.compositeActions.size,
|
|
8
|
+
totalWorkflows: e.scanResult.workflows.size,
|
|
9
|
+
totalActionsChecked: e.actionsToCheckCount,
|
|
10
|
+
totalBlockedByMode: e.blockedByMode.length,
|
|
11
|
+
totalActions: e.scanResult.actions.length,
|
|
12
|
+
totalUpdates: e.outdated.length,
|
|
13
|
+
totalSkipped: e.skipped.length
|
|
14
|
+
},
|
|
15
|
+
options: {
|
|
16
|
+
directories: e.directories.map((e) => serializeDirectoryPath(e, n)),
|
|
17
|
+
excludePatterns: e.excludePatterns,
|
|
18
|
+
includeBranches: e.includeBranches,
|
|
19
|
+
recursive: e.recursive,
|
|
20
|
+
minAge: e.minAge,
|
|
21
|
+
mode: e.mode,
|
|
22
|
+
reportOnly: !0,
|
|
23
|
+
json: !0
|
|
24
|
+
},
|
|
25
|
+
blockedByMode: e.blockedByMode.map((e) => serializeUpdate(e, n)),
|
|
26
|
+
updates: e.outdated.map((e) => serializeUpdate(e, n)),
|
|
27
|
+
skipped: e.skipped.map((e) => serializeUpdate(e, n)),
|
|
28
|
+
status: e.status,
|
|
29
|
+
schemaVersion: 1
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function serializeUpdate(e, n) {
|
|
33
|
+
return {
|
|
34
|
+
action: {
|
|
35
|
+
file: serializePath(e.action.file, n),
|
|
36
|
+
version: e.action.version ?? null,
|
|
37
|
+
line: e.action.line ?? null,
|
|
38
|
+
uses: e.action.uses ?? null,
|
|
39
|
+
job: e.action.job ?? null,
|
|
40
|
+
ref: e.action.ref ?? null,
|
|
41
|
+
name: e.action.name,
|
|
42
|
+
type: e.action.type
|
|
43
|
+
},
|
|
44
|
+
publishedAt: e.publishedAt?.toISOString() ?? null,
|
|
45
|
+
currentVersion: e.currentVersion,
|
|
46
|
+
skipReason: e.skipReason ?? null,
|
|
47
|
+
latestVersion: e.latestVersion,
|
|
48
|
+
isBreaking: e.isBreaking,
|
|
49
|
+
status: e.status ?? "ok",
|
|
50
|
+
hasUpdate: e.hasUpdate,
|
|
51
|
+
latestSha: e.latestSha
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function serializePath(r, i, a = null) {
|
|
55
|
+
if (!r) return null;
|
|
56
|
+
if (!isAbsolute(r)) return r;
|
|
57
|
+
let o = relative(i, r);
|
|
58
|
+
return o === "" ? a ?? r : o.startsWith("..") || isAbsolute(o) ? r : o;
|
|
59
|
+
}
|
|
60
|
+
function serializeDirectoryPath(r, i) {
|
|
61
|
+
let a = relative(i, r);
|
|
62
|
+
return a === "" ? "." : a.startsWith("..") || isAbsolute(a) ? r : a;
|
|
63
|
+
}
|
|
64
|
+
export { buildJsonReport };
|
package/dist/cli/index.js
CHANGED
|
@@ -8,127 +8,186 @@ import { getUpdateLevel } from "../core/versions/get-update-level.js";
|
|
|
8
8
|
import { applyUpdates } from "../core/ast/update/apply-updates.js";
|
|
9
9
|
import { printSkippedWarning } from "./print-skipped-warning.js";
|
|
10
10
|
import { normalizeUpdateMode } from "./normalize-update-mode.js";
|
|
11
|
+
import { validateCliOptions } from "./validate-cli-options.js";
|
|
11
12
|
import { shouldIgnore } from "../core/ignore/should-ignore.js";
|
|
12
13
|
import { checkUpdates } from "../core/api/check-updates.js";
|
|
13
14
|
import { mergeScanResults } from "./merge-scan-results.js";
|
|
14
15
|
import { printModeWarning } from "./print-mode-warning.js";
|
|
15
16
|
import { scanRecursive } from "../core/scan-recursive.js";
|
|
17
|
+
import { buildJsonReport } from "./build-json-report.js";
|
|
16
18
|
import { scanGitHubActions } from "../core/scan-github-actions.js";
|
|
17
19
|
import "../core/index.js";
|
|
18
20
|
import { version } from "../package.js";
|
|
19
21
|
import { createSpinner } from "nanospinner";
|
|
22
|
+
import { resolve } from "node:path";
|
|
20
23
|
import "node:worker_threads";
|
|
21
24
|
import pc from "picocolors";
|
|
22
25
|
import cac from "cac";
|
|
23
26
|
function run() {
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
recursive: v.recursive,
|
|
27
|
+
let C = cac("actions-up");
|
|
28
|
+
C.help().version(version).option("--dir <directory>", "Directory to scan (repeatable). Default: .github, or . with --recursive").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("--json", "Output update information as machine-readable JSON").option("--min-age <days>", "Minimum age in days for updates (default: 0)", { default: 0 }).option("--mode <mode>", "Update mode: major, minor, or patch (default: major)", { default: "major" }).option("--recursive, -r", "Recursively scan directories for YAML files").option("--yes, -y", "Skip all confirmations").command("", "Update GitHub Actions").action(async (b) => {
|
|
29
|
+
let S = b.json ?? !1, C = null, w = resolveScanDirectories({
|
|
30
|
+
recursive: b.recursive,
|
|
29
31
|
cwd: process.cwd(),
|
|
30
|
-
dir:
|
|
31
|
-
});
|
|
32
|
+
dir: b.dir
|
|
33
|
+
}), T = w.map(({ root: e, dir: f }) => resolve(e, f)), E = b.includeBranches ?? !1, D = normalizeUpdateMode(b.mode), O = [];
|
|
34
|
+
Array.isArray(b.exclude) ? O.push(...b.exclude) : typeof b.exclude == "string" && O.push(b.exclude);
|
|
35
|
+
let k = O.flatMap((e) => e.split(",")).map((e) => e.trim()).filter(Boolean);
|
|
32
36
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
validateCliOptions({
|
|
38
|
+
yes: b.yes,
|
|
39
|
+
json: S
|
|
40
|
+
}), S || (console.info(pc.cyan("\n🚀 Actions Up!\n")), C = createSpinner("Scanning GitHub Actions...").start());
|
|
41
|
+
function g({ actionsToCheckCount: e, blockedByMode: f = [], outdated: p = [], skipped: m = [], scanResult: h, status: g }) {
|
|
42
|
+
process.stdout.write(`${JSON.stringify(buildJsonReport({
|
|
43
|
+
recursive: b.recursive ?? !1,
|
|
44
|
+
excludePatterns: k,
|
|
45
|
+
directories: T,
|
|
46
|
+
minAge: b.minAge,
|
|
47
|
+
actionsToCheckCount: e,
|
|
48
|
+
includeBranches: E,
|
|
49
|
+
blockedByMode: f,
|
|
50
|
+
scanResult: h,
|
|
51
|
+
outdated: p,
|
|
52
|
+
skipped: m,
|
|
53
|
+
status: g,
|
|
54
|
+
mode: D
|
|
55
|
+
}), null, 2)}\n`);
|
|
56
|
+
}
|
|
57
|
+
let y = mergeScanResults(b.recursive ? await Promise.all(w.map(({ root: e, dir: f }) => scanRecursive(e, f))) : await Promise.all(w.map(({ root: e, dir: f }) => scanGitHubActions(e, f)))), x = y.actions.length, O = y.workflows.size, A = y.compositeActions.size;
|
|
58
|
+
if (C?.success(`Found ${pc.yellow(x)} actions in ${pc.yellow(O)} workflows and ${pc.yellow(A)} composite actions`), x === 0) {
|
|
59
|
+
if (S) {
|
|
60
|
+
g({
|
|
61
|
+
status: "no-actions-found",
|
|
62
|
+
actionsToCheckCount: 0,
|
|
63
|
+
scanResult: y
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
35
67
|
console.info(pc.green("\n✨ No GitHub Actions found in this repository"));
|
|
36
68
|
return;
|
|
37
69
|
}
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let { name: d } = t;
|
|
45
|
-
for (let t of u) if (t.test(d)) return !1;
|
|
70
|
+
let j = y.actions;
|
|
71
|
+
if (k.length > 0) {
|
|
72
|
+
let { parseExcludePatterns: e } = await import("../core/filters/parse-exclude-patterns.js"), f = e(k);
|
|
73
|
+
f.length > 0 && (j = j.filter((e) => {
|
|
74
|
+
let { name: p } = e;
|
|
75
|
+
for (let e of f) if (e.test(p)) return !1;
|
|
46
76
|
return !0;
|
|
47
77
|
}));
|
|
48
78
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
79
|
+
if (S || (C = createSpinner("Checking for updates...").start()), j.length === 0) {
|
|
80
|
+
if (C?.success("No actions to check after excludes"), S) {
|
|
81
|
+
g({
|
|
82
|
+
status: "nothing-to-check",
|
|
83
|
+
actionsToCheckCount: 0,
|
|
84
|
+
scanResult: y
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.info(pc.green("\n✨ Nothing to check after excludes\n"));
|
|
51
89
|
return;
|
|
52
90
|
}
|
|
53
|
-
let
|
|
54
|
-
client:
|
|
55
|
-
includeBranches:
|
|
56
|
-
}),
|
|
57
|
-
await Promise.all(
|
|
58
|
-
await shouldIgnore(
|
|
91
|
+
let M = process.env.GITHUB_TOKEN, N = createGitHubClient(M), P = await checkUpdates(j, M, {
|
|
92
|
+
client: N,
|
|
93
|
+
includeBranches: E
|
|
94
|
+
}), F = [];
|
|
95
|
+
await Promise.all(P.map(async (e) => {
|
|
96
|
+
await shouldIgnore(e.action.file, e.action.line) || F.push(e);
|
|
59
97
|
}));
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
if (
|
|
64
|
-
let
|
|
65
|
-
let
|
|
66
|
-
if (isSha(
|
|
67
|
-
let
|
|
68
|
-
|
|
98
|
+
let I = F.filter((e) => e.status === "skipped"), L = F.filter((e) => e.hasUpdate), R = b.minAge * 24 * 60 * 60 * 1e3, z = Date.now();
|
|
99
|
+
L = L.filter((e) => e.publishedAt ? z - e.publishedAt.getTime() >= R : !0);
|
|
100
|
+
let B = [];
|
|
101
|
+
if (D !== "major") {
|
|
102
|
+
let p = /* @__PURE__ */ new Map(), h = /* @__PURE__ */ new Map(), g = /* @__PURE__ */ new Map(), _ = await Promise.all(L.map(async (p) => {
|
|
103
|
+
let m = p.currentVersion;
|
|
104
|
+
if (isSha(p.currentVersion)) {
|
|
105
|
+
let f = await readInlineVersionComment(p.action.file, p.action.line, g);
|
|
106
|
+
f && (m = f);
|
|
69
107
|
}
|
|
70
|
-
let
|
|
108
|
+
let h = getUpdateLevel(m, p.latestVersion);
|
|
71
109
|
return {
|
|
72
|
-
effectiveCurrentVersion:
|
|
73
|
-
allowed:
|
|
74
|
-
update:
|
|
110
|
+
effectiveCurrentVersion: m,
|
|
111
|
+
allowed: D === "minor" ? h === "minor" || h === "patch" || h === "none" : h === "patch" || h === "none",
|
|
112
|
+
update: p
|
|
75
113
|
};
|
|
76
|
-
})),
|
|
77
|
-
if (
|
|
78
|
-
let
|
|
79
|
-
currentVersion:
|
|
80
|
-
actionName:
|
|
81
|
-
tagsCache:
|
|
82
|
-
shaCache:
|
|
83
|
-
mode:
|
|
114
|
+
})), v = [], y = await Promise.all(_.map(async (e) => {
|
|
115
|
+
if (e.allowed) return { update: e.update };
|
|
116
|
+
let f = await getCompatibleUpdate(N, {
|
|
117
|
+
currentVersion: e.effectiveCurrentVersion,
|
|
118
|
+
actionName: e.update.action.name,
|
|
119
|
+
tagsCache: p,
|
|
120
|
+
shaCache: h,
|
|
121
|
+
mode: D
|
|
84
122
|
});
|
|
85
|
-
return
|
|
86
|
-
...
|
|
87
|
-
latestVersion:
|
|
88
|
-
latestSha:
|
|
123
|
+
return f ? { update: {
|
|
124
|
+
...e.update,
|
|
125
|
+
latestVersion: f.version,
|
|
126
|
+
latestSha: f.sha,
|
|
89
127
|
isBreaking: !1,
|
|
90
128
|
hasUpdate: !0
|
|
91
|
-
} } : { blocked:
|
|
129
|
+
} } : { blocked: e.update };
|
|
92
130
|
}));
|
|
93
|
-
for (let
|
|
94
|
-
if (
|
|
95
|
-
|
|
131
|
+
for (let e of y) {
|
|
132
|
+
if (e.update) {
|
|
133
|
+
v.push(e.update);
|
|
96
134
|
continue;
|
|
97
135
|
}
|
|
98
|
-
|
|
136
|
+
B.push(e.blocked);
|
|
137
|
+
}
|
|
138
|
+
L = v;
|
|
139
|
+
}
|
|
140
|
+
let V = L.filter((e) => e.isBreaking);
|
|
141
|
+
if (L.length === 0) {
|
|
142
|
+
if (C?.success("All actions are up to date!"), S) {
|
|
143
|
+
g({
|
|
144
|
+
actionsToCheckCount: j.length,
|
|
145
|
+
status: "up-to-date",
|
|
146
|
+
blockedByMode: B,
|
|
147
|
+
scanResult: y,
|
|
148
|
+
skipped: I
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
99
151
|
}
|
|
100
|
-
|
|
152
|
+
I.length > 0 && printSkippedWarning(I, E), B.length > 0 && printModeWarning(B, D), console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
|
|
153
|
+
return;
|
|
101
154
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
155
|
+
if (C?.success(`Found ${pc.yellow(L.length)} updates available${V.length > 0 ? ` (${pc.redBright(V.length)} breaking)` : ""}`), S) {
|
|
156
|
+
g({
|
|
157
|
+
actionsToCheckCount: j.length,
|
|
158
|
+
status: "updates-available",
|
|
159
|
+
blockedByMode: B,
|
|
160
|
+
scanResult: y,
|
|
161
|
+
outdated: L,
|
|
162
|
+
skipped: I
|
|
163
|
+
});
|
|
105
164
|
return;
|
|
106
165
|
}
|
|
107
|
-
if (
|
|
166
|
+
if (I.length > 0 && printSkippedWarning(I, E), B.length > 0 && printModeWarning(B, D), b.dryRun) {
|
|
108
167
|
console.info(pc.yellow("\n📋 Dry Run - No changes will be made\n"));
|
|
109
|
-
for (let
|
|
110
|
-
console.info(pc.gray(`\n${
|
|
168
|
+
for (let e of L) 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`);
|
|
169
|
+
console.info(pc.gray(`\n${L.length} actions would be updated\n`));
|
|
111
170
|
return;
|
|
112
171
|
}
|
|
113
|
-
if (
|
|
114
|
-
let
|
|
115
|
-
if (
|
|
172
|
+
if (b.yes) {
|
|
173
|
+
let e = L.filter((e) => e.latestSha);
|
|
174
|
+
if (e.length === 0) {
|
|
116
175
|
console.info(pc.yellow("\n⚠️ No actions with SHA available for update\n"));
|
|
117
176
|
return;
|
|
118
177
|
}
|
|
119
|
-
console.info(pc.yellow(`\n🔄 Updating ${
|
|
178
|
+
console.info(pc.yellow(`\n🔄 Updating ${e.length} actions...\n`)), await applyUpdates(e), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
120
179
|
} else {
|
|
121
|
-
(
|
|
122
|
-
let
|
|
123
|
-
if (!
|
|
180
|
+
(I.length > 0 || B.length > 0) && console.info("");
|
|
181
|
+
let e = await promptUpdateSelection(L, { showAge: b.minAge > 0 });
|
|
182
|
+
if (!e || e.length === 0) {
|
|
124
183
|
console.info(pc.gray("\nNo updates applied"));
|
|
125
184
|
return;
|
|
126
185
|
}
|
|
127
|
-
console.info(pc.yellow(`\n🔄 Updating ${
|
|
186
|
+
console.info(pc.yellow(`\n🔄 Updating ${e.length} selected actions...\n`)), await applyUpdates(e), console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
128
187
|
}
|
|
129
|
-
} catch (
|
|
130
|
-
|
|
188
|
+
} catch (e) {
|
|
189
|
+
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);
|
|
131
190
|
}
|
|
132
|
-
}),
|
|
191
|
+
}), C.parse();
|
|
133
192
|
}
|
|
134
193
|
export { run };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal subset of CLI flags that require cross-option validation.
|
|
3
|
+
*/
|
|
4
|
+
interface ValidateCliOptionsInput {
|
|
5
|
+
/**
|
|
6
|
+
* Whether JSON report mode is enabled.
|
|
7
|
+
*/
|
|
8
|
+
json?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Whether auto-apply mode is enabled.
|
|
11
|
+
*/
|
|
12
|
+
yes?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate combinations of CLI flags before running the pipeline.
|
|
16
|
+
*
|
|
17
|
+
* @param options - Parsed CLI flags relevant to cross-option validation.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateCliOptions(options: ValidateCliOptionsInput): void;
|
|
20
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isYamlFile } from "./is-yaml-file.js";
|
|
2
|
-
import { lstat, readdir } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
3
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
4
4
|
async function findYamlFilesRecursive(i) {
|
|
5
5
|
let a = [], o = /* @__PURE__ */ new Set();
|
|
6
6
|
async function s(i) {
|
|
@@ -4,10 +4,10 @@ import { GITHUB_DIRECTORY } from "../constants.js";
|
|
|
4
4
|
import { isSha } from "../versions/is-sha.js";
|
|
5
5
|
import { stripAnsi } from "./strip-ansi.js";
|
|
6
6
|
import { padString } from "./pad-string.js";
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import "node:worker_threads";
|
|
8
9
|
import pc from "picocolors";
|
|
9
10
|
import enquirer from "enquirer";
|
|
10
|
-
import path from "node:path";
|
|
11
11
|
var MIN_ACTION_WIDTH = 40, MIN_JOB_WIDTH = 4, MIN_CURRENT_WIDTH = 16, MAX_VERSION_WIDTH = 7;
|
|
12
12
|
async function promptUpdateSelection(g, v = {}) {
|
|
13
13
|
let { showAge: y = !1 } = v;
|
|
@@ -41,8 +41,8 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
41
41
|
};
|
|
42
42
|
})), C = [], w = stripAnsi("Action").length, T = stripAnsi("Current").length, E = stripAnsi("Job").length, D = 0, O = !1;
|
|
43
43
|
for (let [t, a] of b.entries()) {
|
|
44
|
-
let o = a.action.name,
|
|
45
|
-
if (w = Math.max(w, o.length), T = Math.max(T, stripAnsi(d).length,
|
|
44
|
+
let o = a.action.name, l = S[t], d = l.display, f = a.action.job ?? "–";
|
|
45
|
+
if (w = Math.max(w, o.length), T = Math.max(T, stripAnsi(d).length, l.versionForPadding && l.shortSha ? stripAnsi(`${padString(l.versionForPadding, D + 1)}${pc.gray(`(${l.shortSha})`)}`).length : 0), E = Math.max(E, f.length), a.latestVersion) {
|
|
46
46
|
let o = formatVersion(a.latestVersion, S[t]?.effectiveForDiff ?? a.currentVersion);
|
|
47
47
|
D = Math.max(D, stripAnsi(o).length);
|
|
48
48
|
}
|
|
@@ -56,7 +56,7 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
56
56
|
console.warn(`Unexpected missing group for file: ${a}`);
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
|
-
let s = [],
|
|
59
|
+
let s = [], l = o;
|
|
60
60
|
s.push({
|
|
61
61
|
current: "Current",
|
|
62
62
|
action: "Action",
|
|
@@ -65,10 +65,10 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
65
65
|
job: "Job",
|
|
66
66
|
age: "Age"
|
|
67
67
|
});
|
|
68
|
-
for (let { update: t, index: a } of
|
|
69
|
-
let o = !!t.latestSha,
|
|
70
|
-
|
|
71
|
-
let f =
|
|
68
|
+
for (let { update: t, index: a } of l) {
|
|
69
|
+
let o = !!t.latestSha, l = S[a], d = l.display;
|
|
70
|
+
l.versionForPadding && l.shortSha && (d = `${padString(l.versionForPadding, M + 1)}${pc.gray(`(${l.shortSha})`)}`);
|
|
71
|
+
let f = l.effectiveForDiff ?? t.currentVersion, p = formatVersion(t.latestVersion, f), m = t.action.name;
|
|
72
72
|
if (t.latestSha) {
|
|
73
73
|
let i = t.latestSha.slice(0, 7);
|
|
74
74
|
p = `${padString(p, M + 1)}${pc.gray(`(${i})`)}`;
|
|
@@ -101,7 +101,7 @@ async function promptUpdateSelection(g, v = {}) {
|
|
|
101
101
|
name: ""
|
|
102
102
|
});
|
|
103
103
|
else {
|
|
104
|
-
let { update: i, index: a } =
|
|
104
|
+
let { update: i, index: a } = l[t - 1], s = !!i.latestSha, c = s && !i.isBreaking;
|
|
105
105
|
_.push({
|
|
106
106
|
message: o,
|
|
107
107
|
value: String(a),
|
|
@@ -2,17 +2,17 @@ import { ACTIONS_DIRECTORY, GITHUB_DIRECTORY, WORKFLOWS_DIRECTORY } from "./cons
|
|
|
2
2
|
import { isYamlFile } from "./fs/is-yaml-file.js";
|
|
3
3
|
import { scanWorkflowFile } from "./scan-workflow-file.js";
|
|
4
4
|
import { scanActionFile } from "./scan-action-file.js";
|
|
5
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
5
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
-
|
|
6
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
|
+
async function scanGitHubActions(p = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
8
8
|
let h = {
|
|
9
9
|
compositeActions: /* @__PURE__ */ new Map(),
|
|
10
10
|
workflows: /* @__PURE__ */ new Map(),
|
|
11
11
|
actions: []
|
|
12
|
-
}, g = resolve(
|
|
13
|
-
function _(e,
|
|
14
|
-
let
|
|
15
|
-
return
|
|
12
|
+
}, g = resolve(p);
|
|
13
|
+
function _(e, a) {
|
|
14
|
+
let o = relative(e, a);
|
|
15
|
+
return o !== "" && !o.startsWith("..") && !isAbsolute(o);
|
|
16
16
|
}
|
|
17
17
|
let v = join(g, m);
|
|
18
18
|
function y(e) {
|
|
@@ -20,8 +20,8 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
20
20
|
}
|
|
21
21
|
async function b(e) {
|
|
22
22
|
try {
|
|
23
|
-
let
|
|
24
|
-
return typeof
|
|
23
|
+
let a = await stat(e);
|
|
24
|
+
return typeof a.isFile == "function" ? a.isFile() : !1;
|
|
25
25
|
} catch {
|
|
26
26
|
return !1;
|
|
27
27
|
}
|
|
@@ -30,13 +30,13 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
30
30
|
try {
|
|
31
31
|
if ((await stat(x)).isDirectory()) {
|
|
32
32
|
let e = (await readdir(x)).filter((e) => y(e) ? isYamlFile(e) : !1).map(async (e) => {
|
|
33
|
-
let
|
|
33
|
+
let a = join(x, e);
|
|
34
34
|
try {
|
|
35
|
-
let
|
|
35
|
+
let s = await scanWorkflowFile(a);
|
|
36
36
|
return {
|
|
37
37
|
path: `${m}/${WORKFLOWS_DIRECTORY}/${e}`,
|
|
38
38
|
success: !0,
|
|
39
|
-
actions:
|
|
39
|
+
actions: s
|
|
40
40
|
};
|
|
41
41
|
} catch {
|
|
42
42
|
return {
|
|
@@ -45,111 +45,111 @@ async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
|
|
|
45
45
|
actions: []
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
}),
|
|
49
|
-
for (let e of
|
|
48
|
+
}), a = await Promise.all(e);
|
|
49
|
+
for (let e of a) 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, []));
|
|
50
50
|
}
|
|
51
51
|
} catch {}
|
|
52
52
|
try {
|
|
53
|
-
let e = join(g, "action.yml"),
|
|
53
|
+
let e = join(g, "action.yml"), a = join(g, "action.yaml"), o = null, s = [];
|
|
54
54
|
if (await b(e)) try {
|
|
55
|
-
|
|
55
|
+
s = await scanActionFile(e), o = e;
|
|
56
56
|
} catch {
|
|
57
|
-
|
|
57
|
+
o = null;
|
|
58
58
|
}
|
|
59
|
-
if (!
|
|
60
|
-
|
|
59
|
+
if (!o && await b(a)) try {
|
|
60
|
+
s = await scanActionFile(a), o = a;
|
|
61
61
|
} catch {
|
|
62
|
-
|
|
62
|
+
o = null;
|
|
63
63
|
}
|
|
64
|
-
if (
|
|
65
|
-
let e = relative(g,
|
|
66
|
-
h.compositeActions.set(e, e),
|
|
64
|
+
if (o) {
|
|
65
|
+
let e = relative(g, o);
|
|
66
|
+
h.compositeActions.set(e, e), s.length > 0 && h.actions.push(...s);
|
|
67
67
|
}
|
|
68
68
|
} catch {}
|
|
69
69
|
let S = join(v, ACTIONS_DIRECTORY);
|
|
70
70
|
try {
|
|
71
71
|
if ((await stat(S)).isDirectory()) {
|
|
72
|
-
let
|
|
73
|
-
if (!y(
|
|
74
|
-
let
|
|
72
|
+
let a = (await readdir(S)).map(async (a) => {
|
|
73
|
+
if (!y(a)) return null;
|
|
74
|
+
let o = join(S, a);
|
|
75
75
|
try {
|
|
76
|
-
if (!(await stat(
|
|
77
|
-
let
|
|
76
|
+
if (!(await stat(o)).isDirectory()) return null;
|
|
77
|
+
let s = join(o, "action.yml"), c = [];
|
|
78
78
|
try {
|
|
79
|
-
|
|
79
|
+
c = await scanActionFile(s);
|
|
80
80
|
} catch {
|
|
81
81
|
try {
|
|
82
|
-
|
|
82
|
+
s = join(o, "action.yaml"), c = await scanActionFile(s);
|
|
83
83
|
} catch {
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
return {
|
|
88
|
-
path: `${m}/${ACTIONS_DIRECTORY}/${
|
|
89
|
-
name:
|
|
90
|
-
actions:
|
|
88
|
+
path: `${m}/${ACTIONS_DIRECTORY}/${a}`,
|
|
89
|
+
name: a,
|
|
90
|
+
actions: c
|
|
91
91
|
};
|
|
92
92
|
} catch {
|
|
93
93
|
return null;
|
|
94
94
|
}
|
|
95
|
-
}),
|
|
96
|
-
for (let e of
|
|
95
|
+
}), o = await Promise.all(a);
|
|
96
|
+
for (let e of o) e && (h.compositeActions.set(e.name, e.path), h.actions.push(...e.actions));
|
|
97
97
|
}
|
|
98
98
|
} catch {}
|
|
99
99
|
try {
|
|
100
100
|
let e = await getCurrentRepoSlug(g);
|
|
101
101
|
if (e) {
|
|
102
102
|
if (process.env.ACTIONS_UP_TEST_THROW === "1") throw Error("test");
|
|
103
|
-
let
|
|
104
|
-
for (let
|
|
105
|
-
if (
|
|
106
|
-
let
|
|
107
|
-
if (
|
|
108
|
-
let
|
|
109
|
-
_(g,
|
|
103
|
+
let a = /* @__PURE__ */ new Set(), o = [];
|
|
104
|
+
for (let s of h.actions) {
|
|
105
|
+
if (s.type !== "external") continue;
|
|
106
|
+
let c = s.name.split("/");
|
|
107
|
+
if (c.length < 3 || `${c[0]}/${c[1]}` !== e) continue;
|
|
108
|
+
let l = join(g, ...c.slice(2));
|
|
109
|
+
_(g, l) && (a.has(l) || (a.add(l), o.push(l)));
|
|
110
110
|
}
|
|
111
|
-
async function
|
|
112
|
-
if (
|
|
113
|
-
let
|
|
111
|
+
async function s() {
|
|
112
|
+
if (o.length === 0) return;
|
|
113
|
+
let c = o.splice(0), u = await Promise.all(c.map(async (o) => {
|
|
114
114
|
try {
|
|
115
|
-
let
|
|
115
|
+
let s = join(o, "action.yml"), c = join(o, "action.yaml"), u = s;
|
|
116
116
|
try {
|
|
117
|
-
if (!(await stat(
|
|
117
|
+
if (!(await stat(s)).isFile()) throw Error("not a file");
|
|
118
118
|
} catch {
|
|
119
|
-
if (!(await stat(
|
|
120
|
-
|
|
119
|
+
if (!(await stat(c)).isFile()) throw Error("not a file");
|
|
120
|
+
u = c;
|
|
121
121
|
}
|
|
122
|
-
let
|
|
123
|
-
|
|
124
|
-
let
|
|
125
|
-
for (let
|
|
126
|
-
if (
|
|
127
|
-
let
|
|
128
|
-
if (
|
|
129
|
-
let
|
|
130
|
-
_(g,
|
|
122
|
+
let d = await scanActionFile(u);
|
|
123
|
+
d.length > 0 && h.actions.push(...d);
|
|
124
|
+
let f = [];
|
|
125
|
+
for (let o of d) {
|
|
126
|
+
if (o.type !== "external") continue;
|
|
127
|
+
let s = o.name.split("/");
|
|
128
|
+
if (s.length < 3 || `${s[0]}/${s[1]}` !== e) continue;
|
|
129
|
+
let c = join(g, ...s.slice(2));
|
|
130
|
+
_(g, c) && (a.has(c) || (a.add(c), f.push(c)));
|
|
131
131
|
}
|
|
132
|
-
return
|
|
132
|
+
return f;
|
|
133
133
|
} catch {
|
|
134
134
|
return [];
|
|
135
135
|
}
|
|
136
136
|
}));
|
|
137
|
-
for (let e of
|
|
138
|
-
await
|
|
137
|
+
for (let e of u) for (let a of e) o.push(a);
|
|
138
|
+
await s();
|
|
139
139
|
}
|
|
140
|
-
await
|
|
140
|
+
await s();
|
|
141
141
|
}
|
|
142
142
|
} catch {}
|
|
143
143
|
return h;
|
|
144
144
|
}
|
|
145
145
|
async function getCurrentRepoSlug(e) {
|
|
146
|
-
let
|
|
147
|
-
if (
|
|
146
|
+
let a = process.env.GITHUB_REPOSITORY;
|
|
147
|
+
if (a && /^[^\s/]+\/[^\s/]+$/u.test(a)) return a;
|
|
148
148
|
try {
|
|
149
|
-
let
|
|
150
|
-
if (
|
|
151
|
-
let
|
|
152
|
-
if (
|
|
149
|
+
let a = await readFile(join(e, ".git", "config"), "utf8"), o = a.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim();
|
|
150
|
+
if (o ||= a.match(/url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim(), !o) return null;
|
|
151
|
+
let s = o.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^./]+)(?:\.git)?$/u);
|
|
152
|
+
if (s?.groups) return `${s.groups.owner}/${s.groups.repo}`;
|
|
153
153
|
} catch {}
|
|
154
154
|
return null;
|
|
155
155
|
}
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "1.
|
|
1
|
+
const version = "1.13.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -144,6 +144,17 @@ Check for updates without making any changes:
|
|
|
144
144
|
npx actions-up --dry-run
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
### JSON Mode
|
|
148
|
+
|
|
149
|
+
Output a machine-readable JSON report instead of the interactive UI:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npx actions-up --json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`--json` is report-only: it never writes files, skips the interactive prompt,
|
|
156
|
+
and cannot be combined with `--yes`.
|
|
157
|
+
|
|
147
158
|
### Custom Directory
|
|
148
159
|
|
|
149
160
|
By default, Actions Up scans `.github`.
|
|
@@ -235,69 +246,53 @@ jobs:
|
|
|
235
246
|
echo "## GitHub Actions Update Check" >> $GITHUB_STEP_SUMMARY
|
|
236
247
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
237
248
|
|
|
238
|
-
#
|
|
239
|
-
HAS_UPDATES=false
|
|
240
|
-
UPDATE_COUNT=0
|
|
241
|
-
|
|
242
|
-
# Run actions-up and capture output
|
|
249
|
+
# Run actions-up and capture machine-readable output
|
|
243
250
|
echo "Running actions-up to check for updates..."
|
|
244
|
-
actions-up --
|
|
251
|
+
actions-up --json > actions-up-report.json
|
|
245
252
|
|
|
246
|
-
|
|
247
|
-
if grep -q "→" actions-up-raw.txt; then
|
|
248
|
-
HAS_UPDATES=true
|
|
249
|
-
# Count the number of updates (lines with arrows)
|
|
250
|
-
UPDATE_COUNT=$(grep -c "→" actions-up-raw.txt || echo "0")
|
|
251
|
-
fi
|
|
253
|
+
UPDATE_COUNT=$(node -pe "JSON.parse(require('node:fs').readFileSync('actions-up-report.json', 'utf8')).summary.totalUpdates")
|
|
252
254
|
|
|
253
255
|
# Create formatted output
|
|
254
|
-
if [ "$
|
|
256
|
+
if [ "$UPDATE_COUNT" -gt 0 ]; then
|
|
255
257
|
echo "Found $UPDATE_COUNT GitHub Actions with available updates" >> $GITHUB_STEP_SUMMARY
|
|
256
258
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
257
259
|
echo "<details>" >> $GITHUB_STEP_SUMMARY
|
|
258
|
-
echo "<summary>Click to see
|
|
260
|
+
echo "<summary>Click to see JSON report</summary>" >> $GITHUB_STEP_SUMMARY
|
|
259
261
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
260
|
-
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
261
|
-
cat actions-up-
|
|
262
|
+
echo '```json' >> $GITHUB_STEP_SUMMARY
|
|
263
|
+
cat actions-up-report.json >> $GITHUB_STEP_SUMMARY
|
|
262
264
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
263
265
|
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
|
264
266
|
|
|
265
267
|
# Create detailed markdown report with better formatting
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
node --input-type=module <<'EOF'
|
|
269
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
270
|
+
|
|
271
|
+
let report = JSON.parse(readFileSync('actions-up-report.json', 'utf8'))
|
|
272
|
+
let lines = [
|
|
273
|
+
'## GitHub Actions Update Report',
|
|
274
|
+
'',
|
|
275
|
+
'### Summary',
|
|
276
|
+
`- **Updates available:** ${report.summary.totalUpdates}`,
|
|
277
|
+
'',
|
|
278
|
+
'### Updates',
|
|
279
|
+
'',
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
for (let update of report.updates) {
|
|
283
|
+
let file = update.action.file ?? 'unknown'
|
|
284
|
+
let currentVersion = update.currentVersion ?? 'unknown'
|
|
285
|
+
let latestVersion = update.latestVersion ?? 'unknown'
|
|
286
|
+
lines.push(
|
|
287
|
+
`- \`${update.action.name}\` in \`${file}\`: \`${currentVersion}\` → \`${latestVersion}\``,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
269
290
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
echo ""
|
|
291
|
+
lines.push('')
|
|
292
|
+
lines.push('Run `npx actions-up` locally to review and apply updates.')
|
|
273
293
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
echo ""
|
|
277
|
-
echo "Choose from several ways to update these actions:"
|
|
278
|
-
echo ""
|
|
279
|
-
echo "#### Option 1: Automatic Update (Recommended)"
|
|
280
|
-
echo '```bash'
|
|
281
|
-
echo "# Run this command locally in your repository"
|
|
282
|
-
echo "npx actions-up"
|
|
283
|
-
echo '```'
|
|
284
|
-
echo ""
|
|
285
|
-
echo "#### Option 2: Manual Update"
|
|
286
|
-
echo "1. Review each update in the table above"
|
|
287
|
-
echo "2. For breaking changes, click the Release Notes link to review changes"
|
|
288
|
-
echo "3. Edit the workflows and update the version numbers"
|
|
289
|
-
echo "4. Test the changes in your CI/CD pipeline"
|
|
290
|
-
echo ""
|
|
291
|
-
echo "---"
|
|
292
|
-
echo ""
|
|
293
|
-
echo "<details>"
|
|
294
|
-
echo "<summary>Raw actions-up output</summary>"
|
|
295
|
-
echo ""
|
|
296
|
-
echo '```'
|
|
297
|
-
cat actions-up-raw.txt
|
|
298
|
-
echo '```'
|
|
299
|
-
echo "</details>"
|
|
300
|
-
} > actions-up-report.md
|
|
294
|
+
writeFileSync('actions-up-report.md', lines.join('\n'))
|
|
295
|
+
EOF
|
|
301
296
|
|
|
302
297
|
echo "has-updates=true" >> $GITHUB_OUTPUT
|
|
303
298
|
echo "update-count=$UPDATE_COUNT" >> $GITHUB_OUTPUT
|
|
@@ -470,7 +465,7 @@ Or in GitHub Actions:
|
|
|
470
465
|
- name: Check for updates
|
|
471
466
|
env:
|
|
472
467
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
473
|
-
run: npx actions-up --
|
|
468
|
+
run: npx actions-up --json
|
|
474
469
|
```
|
|
475
470
|
|
|
476
471
|
### Skipping Updates
|