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.
@@ -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 b = cac("actions-up");
25
- b.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("--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 (v) => {
26
- console.info(pc.cyan("\n🚀 Actions Up!\n"));
27
- let y = createSpinner("Scanning GitHub Actions...").start(), b = resolveScanDirectories({
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: v.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
- let m = mergeScanResults(v.recursive ? await Promise.all(b.map(({ root: t, dir: u }) => scanRecursive(t, u))) : await Promise.all(b.map(({ root: t, dir: u }) => scanGitHubActions(t, u)))), x = m.actions.length, S = m.workflows.size, C = m.compositeActions.size;
34
- if (y.success(`Found ${pc.yellow(x)} actions in ${pc.yellow(S)} workflows and ${pc.yellow(C)} composite actions`), x === 0) {
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 w = m.actions, T = [];
39
- Array.isArray(v.exclude) ? T.push(...v.exclude) : typeof v.exclude == "string" && T.push(v.exclude);
40
- let E = T.flatMap((t) => t.split(",")).map((t) => t.trim()).filter(Boolean);
41
- if (E.length > 0) {
42
- let { parseExcludePatterns: t } = await import("../core/filters/parse-exclude-patterns.js"), u = t(E);
43
- u.length > 0 && (w = w.filter((t) => {
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 (y = createSpinner("Checking for updates...").start(), w.length === 0) {
50
- y.success("No actions to check after excludes"), console.info(pc.green("\n✨ Nothing to check after excludes\n"));
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 D = process.env.GITHUB_TOKEN, O = createGitHubClient(D), k = v.includeBranches ?? !1, A = await checkUpdates(w, D, {
54
- client: O,
55
- includeBranches: k
56
- }), j = [];
57
- await Promise.all(A.map(async (t) => {
58
- await shouldIgnore(t.action.file, t.action.line) || j.push(t);
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 M = j.filter((t) => t.status === "skipped"), N = j.filter((t) => t.hasUpdate), P = v.minAge * 24 * 60 * 60 * 1e3, F = Date.now();
61
- N = N.filter((t) => t.publishedAt ? F - t.publishedAt.getTime() >= P : !0);
62
- let I = normalizeUpdateMode(v.mode), L = [];
63
- if (I !== "major") {
64
- let d = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map(), h = await Promise.all(N.map(async (d) => {
65
- let f = d.currentVersion;
66
- if (isSha(d.currentVersion)) {
67
- let u = await readInlineVersionComment(d.action.file, d.action.line, m);
68
- u && (f = u);
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 p = getUpdateLevel(f, d.latestVersion);
108
+ let h = getUpdateLevel(m, p.latestVersion);
71
109
  return {
72
- effectiveCurrentVersion: f,
73
- allowed: I === "minor" ? p === "minor" || p === "patch" || p === "none" : p === "patch" || p === "none",
74
- update: d
110
+ effectiveCurrentVersion: m,
111
+ allowed: D === "minor" ? h === "minor" || h === "patch" || h === "none" : h === "patch" || h === "none",
112
+ update: p
75
113
  };
76
- })), g = [], _ = await Promise.all(h.map(async (t) => {
77
- if (t.allowed) return { update: t.update };
78
- let u = await getCompatibleUpdate(O, {
79
- currentVersion: t.effectiveCurrentVersion,
80
- actionName: t.update.action.name,
81
- tagsCache: d,
82
- shaCache: p,
83
- mode: I
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 u ? { update: {
86
- ...t.update,
87
- latestVersion: u.version,
88
- latestSha: u.sha,
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: t.update };
129
+ } } : { blocked: e.update };
92
130
  }));
93
- for (let t of _) {
94
- if (t.update) {
95
- g.push(t.update);
131
+ for (let e of y) {
132
+ if (e.update) {
133
+ v.push(e.update);
96
134
  continue;
97
135
  }
98
- L.push(t.blocked);
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
- N = g;
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
- let R = N.filter((t) => t.isBreaking);
103
- if (N.length === 0) {
104
- y.success("All actions are up to date!"), M.length > 0 && printSkippedWarning(M, k), L.length > 0 && printModeWarning(L, I), console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
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 (y.success(`Found ${pc.yellow(N.length)} updates available${R.length > 0 ? ` (${pc.redBright(R.length)} breaking)` : ""}`), M.length > 0 && printSkippedWarning(M, k), L.length > 0 && printModeWarning(L, I), v.dryRun) {
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 t of N) console.info(`${pc.cyan(t.action.file ?? "unknown")}:\n${t.action.name}: ${pc.redBright(t.currentVersion)} → ${pc.green(t.latestVersion)} ${t.latestSha ? pc.gray(`(${t.latestSha.slice(0, 7)})`) : ""}\n`);
110
- console.info(pc.gray(`\n${N.length} actions would be updated\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 (v.yes) {
114
- let t = N.filter((t) => t.latestSha);
115
- if (t.length === 0) {
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 ${t.length} actions...\n`)), await applyUpdates(t), console.info(pc.green("\n✓ Updates applied successfully!"));
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
- (M.length > 0 || L.length > 0) && console.info("");
122
- let t = await promptUpdateSelection(N, { showAge: v.minAge > 0 });
123
- if (!t || t.length === 0) {
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 ${t.length} selected actions...\n`)), await applyUpdates(t), console.info(pc.green("\n✓ Updates applied successfully!"));
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 (t) {
130
- y.error("Failed"), t instanceof Error && t.name === "GitHubRateLimitError" ? (console.error(pc.yellow("\n⚠️ Rate Limit Exceeded\n")), console.error(t.message), console.error(pc.gray("\nExample: GITHUB_TOKEN=ghp_xxxx actions-up\n"))) : console.error(pc.redBright("\nError:"), t instanceof Error ? t.message : String(t)), process.exit(1);
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
- }), b.parse();
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 {};
@@ -0,0 +1,4 @@
1
+ function validateCliOptions(e) {
2
+ if (e.json && e.yes) throw Error("--json cannot be used with --yes");
3
+ }
4
+ export { validateCliOptions };
@@ -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, u = S[t], d = u.display, f = a.action.job ?? "–";
45
- if (w = Math.max(w, o.length), T = Math.max(T, stripAnsi(d).length, u.versionForPadding && u.shortSha ? stripAnsi(`${padString(u.versionForPadding, D + 1)}${pc.gray(`(${u.shortSha})`)}`).length : 0), E = Math.max(E, f.length), a.latestVersion) {
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 = [], u = o;
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 u) {
69
- let o = !!t.latestSha, u = S[a], d = u.display;
70
- u.versionForPadding && u.shortSha && (d = `${padString(u.versionForPadding, M + 1)}${pc.gray(`(${u.shortSha})`)}`);
71
- let f = u.effectiveForDiff ?? t.currentVersion, p = formatVersion(t.latestVersion, f), m = t.action.name;
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 } = u[t - 1], s = !!i.latestSha, c = s && !i.isBreaking;
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
- async function scanGitHubActions(d = process.cwd(), m = GITHUB_DIRECTORY) {
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(d);
13
- function _(e, o) {
14
- let s = relative(e, o);
15
- return s !== "" && !s.startsWith("..") && !isAbsolute(s);
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 o = await stat(e);
24
- return typeof o.isFile == "function" ? o.isFile() : !1;
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 o = join(x, e);
33
+ let a = join(x, e);
34
34
  try {
35
- let c = await scanWorkflowFile(o);
35
+ let s = await scanWorkflowFile(a);
36
36
  return {
37
37
  path: `${m}/${WORKFLOWS_DIRECTORY}/${e}`,
38
38
  success: !0,
39
- actions: c
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
- }), o = await Promise.all(e);
49
- 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, []));
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"), o = join(g, "action.yaml"), s = null, c = [];
53
+ let e = join(g, "action.yml"), a = join(g, "action.yaml"), o = null, s = [];
54
54
  if (await b(e)) try {
55
- c = await scanActionFile(e), s = e;
55
+ s = await scanActionFile(e), o = e;
56
56
  } catch {
57
- s = null;
57
+ o = null;
58
58
  }
59
- if (!s && await b(o)) try {
60
- c = await scanActionFile(o), s = o;
59
+ if (!o && await b(a)) try {
60
+ s = await scanActionFile(a), o = a;
61
61
  } catch {
62
- s = null;
62
+ o = null;
63
63
  }
64
- if (s) {
65
- let e = relative(g, s);
66
- h.compositeActions.set(e, e), c.length > 0 && h.actions.push(...c);
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 o = (await readdir(S)).map(async (o) => {
73
- if (!y(o)) return null;
74
- let s = join(S, o);
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(s)).isDirectory()) return null;
77
- let c = join(s, "action.yml"), l = [];
76
+ if (!(await stat(o)).isDirectory()) return null;
77
+ let s = join(o, "action.yml"), c = [];
78
78
  try {
79
- l = await scanActionFile(c);
79
+ c = await scanActionFile(s);
80
80
  } catch {
81
81
  try {
82
- c = join(s, "action.yaml"), l = await scanActionFile(c);
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}/${o}`,
89
- name: o,
90
- actions: l
88
+ path: `${m}/${ACTIONS_DIRECTORY}/${a}`,
89
+ name: a,
90
+ actions: c
91
91
  };
92
92
  } catch {
93
93
  return null;
94
94
  }
95
- }), s = await Promise.all(o);
96
- for (let e of s) e && (h.compositeActions.set(e.name, e.path), h.actions.push(...e.actions));
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 o = /* @__PURE__ */ new Set(), s = [];
104
- for (let c of h.actions) {
105
- if (c.type !== "external") continue;
106
- let l = c.name.split("/");
107
- if (l.length < 3 || `${l[0]}/${l[1]}` !== e) continue;
108
- let u = join(g, ...l.slice(2));
109
- _(g, u) && (o.has(u) || (o.add(u), s.push(u)));
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 c() {
112
- if (s.length === 0) return;
113
- let l = s.splice(0), d = await Promise.all(l.map(async (s) => {
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 c = join(s, "action.yml"), l = join(s, "action.yaml"), d = c;
115
+ let s = join(o, "action.yml"), c = join(o, "action.yaml"), u = s;
116
116
  try {
117
- if (!(await stat(c)).isFile()) throw Error("not a file");
117
+ if (!(await stat(s)).isFile()) throw Error("not a file");
118
118
  } catch {
119
- if (!(await stat(l)).isFile()) throw Error("not a file");
120
- d = l;
119
+ if (!(await stat(c)).isFile()) throw Error("not a file");
120
+ u = c;
121
121
  }
122
- let f = await scanActionFile(d);
123
- f.length > 0 && h.actions.push(...f);
124
- let p = [];
125
- for (let s of f) {
126
- if (s.type !== "external") continue;
127
- let c = s.name.split("/");
128
- if (c.length < 3 || `${c[0]}/${c[1]}` !== e) continue;
129
- let l = join(g, ...c.slice(2));
130
- _(g, l) && (o.has(l) || (o.add(l), p.push(l)));
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 p;
132
+ return f;
133
133
  } catch {
134
134
  return [];
135
135
  }
136
136
  }));
137
- for (let e of d) for (let o of e) s.push(o);
138
- await c();
137
+ for (let e of u) for (let a of e) o.push(a);
138
+ await s();
139
139
  }
140
- await c();
140
+ await s();
141
141
  }
142
142
  } catch {}
143
143
  return h;
144
144
  }
145
145
  async function getCurrentRepoSlug(e) {
146
- let o = process.env.GITHUB_REPOSITORY;
147
- if (o && /^[^\s/]+\/[^\s/]+$/u.test(o)) return o;
146
+ let a = process.env.GITHUB_REPOSITORY;
147
+ if (a && /^[^\s/]+\/[^\s/]+$/u.test(a)) return a;
148
148
  try {
149
- let o = await readFile(join(e, ".git", "config"), "utf8"), s = o.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim();
150
- if (s ||= o.match(/url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim(), !s) return null;
151
- let c = s.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^./]+)(?:\.git)?$/u);
152
- if (c?.groups) return `${c.groups.owner}/${c.groups.repo}`;
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.12.1";
1
+ const version = "1.13.0";
2
2
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actions-up",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
4
4
  "description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
5
5
  "keywords": [
6
6
  "github-actions",
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
- # Initialize variables
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 --dry-run > actions-up-raw.txt 2>&1
251
+ actions-up --json > actions-up-report.json
245
252
 
246
- # Parse the output to detect updates
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 [ "$HAS_UPDATES" = true ]; then
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 details</summary>" >> $GITHUB_STEP_SUMMARY
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-raw.txt >> $GITHUB_STEP_SUMMARY
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
- echo "## GitHub Actions Update Report"
268
- echo ""
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
- echo "### Summary"
271
- echo "- **Updates available:** $UPDATE_COUNT"
272
- echo ""
291
+ lines.push('')
292
+ lines.push('Run `npx actions-up` locally to review and apply updates.')
273
293
 
274
- # See the raw output above for details.
275
- echo "### How to Update"
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 --dry-run
468
+ run: npx actions-up --json
474
469
  ```
475
470
 
476
471
  ### Skipping Updates