actions-up 1.13.0 → 1.14.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.
@@ -9,55 +9,55 @@ import "node:worker_threads";
9
9
  import pc from "picocolors";
10
10
  import enquirer from "enquirer";
11
11
  var MIN_ACTION_WIDTH = 40, MIN_JOB_WIDTH = 4, MIN_CURRENT_WIDTH = 16, MAX_VERSION_WIDTH = 7;
12
- async function promptUpdateSelection(g, v = {}) {
13
- let { showAge: y = !1 } = v;
14
- if (g.length === 0) return null;
15
- let b = g.filter((t) => t.hasUpdate);
16
- if (b.length === 0) return console.info(pc.green("✓ All actions are up to date!")), null;
17
- let x = /* @__PURE__ */ new Map();
18
- for (let [t, i] of b.entries()) {
19
- let o = i.action.file ?? "unknown file", s = path.relative(path.join(process.cwd(), GITHUB_DIRECTORY), o);
20
- s === "" && (s = o);
21
- let c = x.get(s) ?? [];
22
- c.push({
23
- update: i,
24
- index: t
25
- }), x.set(s, c);
12
+ async function promptUpdateSelection(b, S = {}) {
13
+ let { showAge: C = !1 } = S;
14
+ if (b.length === 0) return null;
15
+ let w = b.filter((e) => e.hasUpdate);
16
+ if (w.length === 0) return console.info(pc.green("✓ All actions are up to date!")), null;
17
+ let T = /* @__PURE__ */ new Map();
18
+ for (let [e, c] of w.entries()) {
19
+ let u = c.action.file ?? "unknown file", d = path.relative(path.join(process.cwd(), GITHUB_DIRECTORY), u);
20
+ d === "" && (d = u);
21
+ let f = T.get(d) ?? [];
22
+ f.push({
23
+ update: c,
24
+ index: e
25
+ }), T.set(d, f);
26
26
  }
27
- let S = await Promise.all(b.map(async (i) => {
28
- let a = formatVersionOrSha(i.currentVersion), s = i.currentVersion ?? void 0, c = null, l = null;
29
- if (!i.currentVersion || !isSha(i.currentVersion)) return {
30
- versionForPadding: c,
31
- effectiveForDiff: s,
32
- shortSha: l,
33
- display: a
27
+ let E = await Promise.all(w.map(async (c) => {
28
+ let l = formatVersionOrSha(c.currentVersion), d = c.currentVersion ?? void 0, f = null, p = null;
29
+ if (!c.currentVersion || !isSha(c.currentVersion)) return {
30
+ versionForPadding: f,
31
+ effectiveForDiff: d,
32
+ shortSha: p,
33
+ display: l
34
34
  };
35
- let u = await readInlineVersionComment(i.action.file, i.action.line);
36
- return u && (l = i.currentVersion.slice(0, 7), c = formatVersionOrSha(u), a = c, s = u), {
37
- versionForPadding: c,
38
- effectiveForDiff: s,
39
- shortSha: l,
40
- display: a
35
+ let m = await readInlineVersionComment(c.action.file, c.action.line);
36
+ return m && (p = c.currentVersion.slice(0, 7), f = formatVersionOrSha(m), l = f, d = m), {
37
+ versionForPadding: f,
38
+ effectiveForDiff: d,
39
+ shortSha: p,
40
+ display: l
41
41
  };
42
- })), C = [], w = stripAnsi("Action").length, T = stripAnsi("Current").length, E = stripAnsi("Job").length, D = 0, O = !1;
43
- for (let [t, a] of b.entries()) {
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
- let o = formatVersion(a.latestVersion, S[t]?.effectiveForDiff ?? a.currentVersion);
47
- D = Math.max(D, stripAnsi(o).length);
42
+ })), D = [], O = stripAnsi("Action").length, k = stripAnsi("Current").length, A = stripAnsi("Job").length, j = 0, M = !1;
43
+ for (let [e, l] of w.entries()) {
44
+ let u = l.action.name, p = E[e], h = p.display, g = l.action.job ?? "–";
45
+ if (O = Math.max(O, u.length), k = Math.max(k, stripAnsi(h).length, p.versionForPadding && p.shortSha ? stripAnsi(`${padString(p.versionForPadding, j + 1)}${pc.gray(`(${p.shortSha})`)}`).length : 0), A = Math.max(A, g.length), l.latestVersion) {
46
+ let u = formatVersion(l.targetRefStyle === "tag" && l.targetRef ? l.targetRef : l.latestVersion, E[e]?.effectiveForDiff ?? l.currentVersion);
47
+ j = Math.max(j, stripAnsi(u).length);
48
48
  }
49
- let p = S[t]?.versionForPadding;
50
- p && (D = Math.max(D, stripAnsi(p).length)), a.publishedAt && (O = !0);
49
+ let _ = E[e]?.versionForPadding;
50
+ _ && (j = Math.max(j, stripAnsi(_).length)), l.publishedAt && (M = !0);
51
51
  }
52
- 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();
53
- for (let [t, a] of F.entries()) {
54
- let o = x.get(a);
55
- if (!o) {
56
- console.warn(`Unexpected missing group for file: ${a}`);
52
+ let N = Math.max(O, MIN_ACTION_WIDTH), P = Math.max(k, MIN_CURRENT_WIDTH), F = Math.max(A, MIN_JOB_WIDTH), I = Math.min(j, MAX_VERSION_WIDTH), L = I + 1 + 9, R = C && M ? 6 : 0, z = [...T.keys()].toSorted();
53
+ for (let [e, l] of z.entries()) {
54
+ let u = T.get(l);
55
+ if (!u) {
56
+ console.warn(`Unexpected missing group for file: ${l}`);
57
57
  continue;
58
58
  }
59
- let s = [], l = o;
60
- s.push({
59
+ let d = [], p = u;
60
+ d.push({
61
61
  current: "Current",
62
62
  action: "Action",
63
63
  target: "Target",
@@ -65,74 +65,74 @@ async function promptUpdateSelection(g, v = {}) {
65
65
  job: "Job",
66
66
  age: "Age"
67
67
  });
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
- if (t.latestSha) {
73
- let i = t.latestSha.slice(0, 7);
74
- p = `${padString(p, M + 1)}${pc.gray(`(${i})`)}`;
68
+ for (let { update: e, index: l } of p) {
69
+ let u = hasResolvedTarget(e), p = E[l], h = p.display;
70
+ p.versionForPadding && p.shortSha && (h = `${padString(p.versionForPadding, I + 1)}${pc.gray(`(${p.shortSha})`)}`);
71
+ let g = p.effectiveForDiff ?? e.currentVersion, _ = formatVersion(getTargetVersion(e), g), v = e.action.name;
72
+ if (getResolvedTargetStyle(e) === "sha" && getResolvedTarget(e)) {
73
+ let c = getResolvedTarget(e).slice(0, 7);
74
+ _ = `${padString(_, I + 1)}${pc.gray(`(${c})`)}`;
75
75
  }
76
- o || (p = pc.gray(p), d = pc.gray(d), m = pc.gray(m));
77
- let h = t.action.job ?? "–", g = formatAge(t.publishedAt);
78
- s.push({
79
- job: o ? h : pc.gray(h),
80
- age: o ? g : pc.gray(g),
81
- action: m,
82
- target: p,
76
+ u || (_ = pc.gray(_), h = pc.gray(h), v = pc.gray(v));
77
+ let y = e.action.job ?? "–", b = formatAge(e.publishedAt);
78
+ d.push({
79
+ job: u ? y : pc.gray(y),
80
+ age: u ? b : pc.gray(b),
81
+ action: v,
82
+ target: _,
83
83
  arrow: "❯",
84
- current: d
84
+ current: h
85
85
  });
86
86
  }
87
- let d = Math.max(k, MIN_ACTION_WIDTH), h = Math.max(A, MIN_CURRENT_WIDTH), g = Math.max(j, MIN_JOB_WIDTH), _ = [];
88
- for (let [t, i] of s.entries()) {
89
- let a = t === 0, o = formatTableRow({
90
- targetWidth: N,
91
- currentWidth: h,
92
- actionWidth: d,
93
- ageWidth: P,
94
- jobWidth: g,
95
- row: i
87
+ let h = Math.max(N, MIN_ACTION_WIDTH), y = Math.max(P, MIN_CURRENT_WIDTH), b = Math.max(F, MIN_JOB_WIDTH), x = [];
88
+ for (let [e, c] of d.entries()) {
89
+ let l = e === 0, u = formatTableRow({
90
+ targetWidth: L,
91
+ currentWidth: y,
92
+ actionWidth: h,
93
+ ageWidth: R,
94
+ jobWidth: b,
95
+ row: c
96
96
  });
97
- if (a) _.push({
98
- message: pc.gray(` ○ ${o}`),
97
+ if (l) x.push({
98
+ message: pc.gray(` ○ ${u}`),
99
99
  role: "separator",
100
100
  indent: "",
101
101
  name: ""
102
102
  });
103
103
  else {
104
- let { update: i, index: a } = l[t - 1], s = !!i.latestSha, c = s && !i.isBreaking;
105
- _.push({
106
- message: o,
107
- value: String(a),
108
- name: String(a),
109
- disabled: !s,
104
+ let { update: c, index: l } = p[e - 1], d = hasResolvedTarget(c), f = d && !c.isBreaking;
105
+ x.push({
106
+ message: u,
107
+ value: String(l),
108
+ disabled: !d,
109
+ name: String(l),
110
110
  indent: "",
111
- enabled: c
111
+ enabled: f
112
112
  });
113
113
  }
114
114
  }
115
- C.push({
116
- message: pc.gray(a),
117
- value: `label|${a}`,
118
- choices: _,
119
- name: `label|${a}`,
115
+ D.push({
116
+ message: pc.gray(l),
117
+ value: `label|${l}`,
118
+ choices: x,
119
+ name: `label|${l}`,
120
120
  isGroupLabel: !0,
121
121
  enabled: !1
122
- }), t < F.length - 1 && C.push({
122
+ }), e < z.length - 1 && D.push({
123
123
  role: "separator",
124
124
  message: " ",
125
125
  name: ""
126
126
  });
127
127
  }
128
128
  try {
129
- let t = {
130
- indicator(t, i) {
131
- if (i.isGroupLabel) {
132
- let t = (i.choices ?? []).filter((t) => !("role" in t)), a = t.length, o = t.filter((t) => !!t.enabled).length === a ? "●" : "○";
133
- return ` ${pc.gray(o)}`;
129
+ let e = {
130
+ indicator(e, c) {
131
+ if (c.isGroupLabel) {
132
+ let e = (c.choices ?? []).filter((e) => !("role" in e)), l = e.length, u = e.filter((e) => !!e.enabled).length === l ? "●" : "○";
133
+ return ` ${pc.gray(u)}`;
134
134
  }
135
- return ` ${i.enabled ? "●" : "○"}`;
135
+ return ` ${c.enabled ? "●" : "○"}`;
136
136
  },
137
137
  message: `Choose which actions to update (Press ${pc.cyan("<space>")} to select, ${pc.cyan("<a>")} to toggle all, ${pc.cyan("<i>")} to invert selection)`,
138
138
  styles: {
@@ -153,44 +153,56 @@ async function promptUpdateSelection(g, v = {}) {
153
153
  type: "multiselect",
154
154
  name: "selected",
155
155
  pointer: "❯",
156
- choices: C
157
- }, { selected: i } = await enquirer.prompt(t), a = /* @__PURE__ */ new Set();
158
- for (let t of i) {
159
- if (t.startsWith("label|")) {
160
- let i = t.slice(6), o = x.get(i) ?? [];
161
- for (let { update: t, index: i } of o) t.latestSha && a.add(i);
156
+ choices: D
157
+ }, { selected: c } = await enquirer.prompt(e), l = /* @__PURE__ */ new Set();
158
+ for (let e of c) {
159
+ if (e.startsWith("label|")) {
160
+ let c = e.slice(6), u = T.get(c) ?? [];
161
+ for (let { update: e, index: c } of u) hasResolvedTarget(e) && l.add(c);
162
162
  continue;
163
163
  }
164
- let i = Number.parseInt(t, 10);
165
- Number.isFinite(i) && a.add(i);
164
+ let c = Number.parseInt(e, 10);
165
+ Number.isFinite(c) && l.add(c);
166
166
  }
167
- let o = [];
168
- for (let [t, i] of b.entries()) a.has(t) && i.latestSha && o.push(i);
169
- return o.length === 0 ? (console.info(pc.yellow("\nNo actions selected")), null) : o;
170
- } catch (t) {
171
- if (t instanceof Error && (t.message.includes("cancelled") || t.message.includes("ESC") || t.name === "ExitPromptError")) return logSelectionCancelled(), null;
172
- throw console.error(pc.red("Unexpected error during selection:"), t), t;
167
+ let u = [];
168
+ for (let [e, c] of w.entries()) l.has(e) && hasResolvedTarget(c) && u.push(c);
169
+ return u.length === 0 ? (console.info(pc.yellow("\nNo actions selected")), null) : u;
170
+ } catch (e) {
171
+ if (e instanceof Error && (e.message.includes("cancelled") || e.message.includes("ESC") || e.name === "ExitPromptError")) return logSelectionCancelled(), null;
172
+ throw console.error(pc.red("Unexpected error during selection:"), e), e;
173
173
  }
174
174
  }
175
- function formatAge(t) {
176
- if (!t) return "";
177
- let i = Date.now() - t.getTime(), a = Math.floor(i / (1e3 * 60 * 60)), o = Math.floor(a / 24), s = Math.floor(o / 7), c = o % 7;
178
- return s >= 1 ? c > 0 ? `${s}w ${c}d` : `${s}w` : o >= 1 ? `${o}d` : `${a}h`;
175
+ function formatAge(e) {
176
+ if (!e) return "";
177
+ let c = Date.now() - e.getTime(), l = Math.floor(c / (1e3 * 60 * 60)), u = Math.floor(l / 24), d = Math.floor(u / 7), f = u % 7;
178
+ return d >= 1 ? f > 0 ? `${d}w ${f}d` : `${d}w` : u >= 1 ? `${u}d` : `${l}h`;
179
179
  }
180
- function formatTableRow(t) {
181
- let { currentWidth: i, actionWidth: a, targetWidth: o, jobWidth: s, ageWidth: l, row: u } = t, d = [
182
- padString(u.action, a),
183
- padString(u.job, s),
184
- padString(u.current, i),
185
- u.arrow,
186
- padString(u.target, o)
180
+ function formatTableRow(e) {
181
+ let { currentWidth: c, actionWidth: l, targetWidth: u, jobWidth: d, ageWidth: p, row: m } = e, h = [
182
+ padString(m.action, l),
183
+ padString(m.job, d),
184
+ padString(m.current, c),
185
+ m.arrow,
186
+ padString(m.target, u)
187
187
  ];
188
- return l > 0 && d.push(u.age), d.join(" ").replace(/\s+$/u, "");
188
+ return p > 0 && h.push(m.age), h.join(" ").replace(/\s+$/u, "");
189
189
  }
190
- function formatVersionOrSha(t) {
191
- return t ? isSha(t) ? t.slice(0, 7) : t.replace(/^v/u, "") : pc.gray("unknown");
190
+ function formatVersionOrSha(e) {
191
+ return e ? isSha(e) ? e.slice(0, 7) : e.replace(/^v/u, "") : pc.gray("unknown");
192
+ }
193
+ function getTargetVersion(e) {
194
+ return getResolvedTargetStyle(e) === "tag" && getResolvedTarget(e) ? getResolvedTarget(e) : e.latestVersion;
195
+ }
196
+ function getResolvedTargetStyle(e) {
197
+ return e.targetRefStyle ? e.targetRefStyle : e.latestSha ? "sha" : null;
198
+ }
199
+ function getResolvedTarget(e) {
200
+ return e.targetRef ? e.targetRef : e.latestSha;
192
201
  }
193
202
  function logSelectionCancelled() {
194
203
  console.info(`\r\u001B[K${pc.yellow("Selection cancelled")}`);
195
204
  }
205
+ function hasResolvedTarget(e) {
206
+ return !!getResolvedTarget(e);
207
+ }
196
208
  export { promptUpdateSelection };
@@ -0,0 +1,10 @@
1
+ import { ActionUpdate } from '../../types/action-update';
2
+ import { UpdateStyle } from '../../types/update-style';
3
+ /**
4
+ * Resolve the final reference that should be written back to the workflow.
5
+ *
6
+ * @param update - Update entry enriched with lookup data.
7
+ * @param style - Effective update style.
8
+ * @returns Update entry with resolved target reference fields.
9
+ */
10
+ export declare function resolveTargetReference(update: ActionUpdate, style: UpdateStyle): ActionUpdate;
@@ -0,0 +1,24 @@
1
+ function resolveTargetReference(e, t) {
2
+ return e.hasUpdate ? t === "sha" || e.currentRefType === "sha" ? e.latestSha ? {
3
+ ...e,
4
+ targetRef: e.latestSha,
5
+ targetRefStyle: "sha"
6
+ } : {
7
+ ...e,
8
+ targetRefStyle: null,
9
+ targetRef: null
10
+ } : e.currentRefType === "tag" && e.latestVersion ? {
11
+ ...e,
12
+ targetRef: e.latestVersion,
13
+ targetRefStyle: "tag"
14
+ } : {
15
+ ...e,
16
+ targetRefStyle: null,
17
+ targetRef: null
18
+ } : {
19
+ ...e,
20
+ targetRefStyle: null,
21
+ targetRef: null
22
+ };
23
+ }
24
+ export { resolveTargetReference };
package/dist/package.js CHANGED
@@ -1,2 +1,2 @@
1
- const version = "1.13.0";
1
+ const version = "1.14.0";
2
2
  export { version };
@@ -6,7 +6,17 @@ export interface ActionUpdate {
6
6
  /**
7
7
  * Reason for skipping the update check.
8
8
  */
9
- skipReason?: 'unknown' | 'branch'
9
+ skipReason?: 'unsupported-style' | 'unknown' | 'branch'
10
+
11
+ /**
12
+ * Detected style of the current reference in the source file.
13
+ */
14
+ currentRefType?: 'unknown' | 'branch' | 'sha' | 'tag'
15
+
16
+ /**
17
+ * Style of the final reference that should be written back to the file.
18
+ */
19
+ targetRefStyle?: 'sha' | 'tag' | null
10
20
 
11
21
  /**
12
22
  * Current version string.
@@ -23,6 +33,11 @@ export interface ActionUpdate {
23
33
  */
24
34
  status?: 'skipped' | 'ok'
25
35
 
36
+ /**
37
+ * Final reference that should be written back to the file.
38
+ */
39
+ targetRef?: string | null
40
+
26
41
  /**
27
42
  * SHA hash of the latest version.
28
43
  */
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Strategy used when writing updated action references back to workflow files.
3
+ */
4
+ export type UpdateStyle = 'preserve' | 'sha'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actions-up",
3
- "version": "1.13.0",
4
- "description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
3
+ "version": "1.14.0",
4
+ "description": "Interactive CLI tool to update GitHub Actions with SHA pinning or preserved refs",
5
5
  "keywords": [
6
6
  "github-actions",
7
7
  "actions",
package/readme.md CHANGED
@@ -16,7 +16,7 @@ Actions Up scans your workflows and composite actions to discover every
16
16
  referenced GitHub Action, then checks for newer releases.
17
17
 
18
18
  Interactively upgrade and pin actions to exact commit SHAs for secure,
19
- reproducible CI and low-friction maintenance.
19
+ reproducible CI, or preserve tag-style references when you need to stay on tags.
20
20
 
21
21
  ## Features
22
22
 
@@ -25,8 +25,8 @@ reproducible CI and low-friction maintenance.
25
25
  `action.yml`/`action.yaml`)
26
26
  - **Reusable Workflows**: Detects and updates reusable workflow calls at the job
27
27
  level
28
- - **SHA pinning**: Updates actions to use commit SHA instead of tags for better
29
- security
28
+ - **Flexible update styles**: Use SHA pinning by default, or preserve tag-style
29
+ references with `--style preserve`
30
30
  - **Batch Updates**: Update multiple actions at once
31
31
  - **Interactive Selection**: Choose which actions to update
32
32
  - **Breaking Changes Detection**: Warns about major version updates
@@ -124,7 +124,7 @@ This will:
124
124
  plus root `action.yml`/`action.yaml`
125
125
  2. Check for available updates
126
126
  3. Show an interactive list to select updates
127
- 4. Apply selected updates with SHA pinning
127
+ 4. Apply selected updates with SHA pinning by default
128
128
 
129
129
  ### Auto-Update Mode
130
130
 
@@ -200,6 +200,24 @@ In `minor` and `patch` modes, Actions Up tries to find the newest compatible tag
200
200
  first (for example, from `@v4` in `minor` mode it will choose the latest
201
201
  `v4.x.y`). If no compatible version exists, that action is skipped.
202
202
 
203
+ ### Update Style
204
+
205
+ By default, Actions Up writes updates as pinned SHAs:
206
+
207
+ ```bash
208
+ npx actions-up --style sha
209
+ ```
210
+
211
+ Use `--style preserve` to keep the current reference style:
212
+
213
+ ```bash
214
+ npx actions-up --style preserve
215
+ ```
216
+
217
+ `preserve` keeps tag references on tags and SHA references on SHAs. For example,
218
+ `actions/checkout@v5` updates to `actions/checkout@v6.0.2`, while a SHA-pinned
219
+ action continues updating to the latest resolved SHA.
220
+
203
221
  ## GitHub Actions Integration
204
222
 
205
223
  ### Automated PR Checks
@@ -500,7 +518,8 @@ Ignore comments (file/block/next-line/inline):
500
518
  Interactive CLI for developers who want control over GitHub Actions updates.
501
519
 
502
520
  - **vs. Dependabot/Renovate:** Dependabot and Renovate update via pull requests;
503
- Actions Up is an interactive CLI with explicit SHA pinning.
521
+ Actions Up is an interactive CLI with explicit SHA pinning by default and an
522
+ opt-in preserve mode for tag users.
504
523
  - **vs. pinact:** pinact is a CLI to pin and update Actions and reusable
505
524
  workflows; Actions Up adds interactive selection and major update warnings.
506
525
  - **Zero-config:** `npx actions-up` runs immediately.