actions-up 1.3.1 → 1.4.1

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.
Files changed (46) hide show
  1. package/dist/cli/index.js +41 -41
  2. package/dist/core/api/check-updates.js +135 -179
  3. package/dist/core/api/create-github-client.js +28 -29
  4. package/dist/core/api/get-all-releases.js +20 -27
  5. package/dist/core/api/get-all-tags.js +5 -7
  6. package/dist/core/api/get-latest-release.js +16 -19
  7. package/dist/core/api/get-reference-type.js +6 -12
  8. package/dist/core/api/get-tag-info.js +47 -76
  9. package/dist/core/api/get-tag-sha.js +12 -22
  10. package/dist/core/api/internal-rate-limit-error.js +3 -4
  11. package/dist/core/api/make-request.js +18 -21
  12. package/dist/core/api/resolve-github-token-sync.js +20 -26
  13. package/dist/core/api/update-rate-limit-info.js +7 -7
  14. package/dist/core/ast/guards/has-range.js +2 -2
  15. package/dist/core/ast/guards/is-node.js +2 -2
  16. package/dist/core/ast/guards/is-pair.js +2 -2
  17. package/dist/core/ast/guards/is-scalar.js +2 -2
  18. package/dist/core/ast/guards/is-yaml-map.js +2 -2
  19. package/dist/core/ast/guards/is-yaml-sequence.js +2 -2
  20. package/dist/core/ast/scanners/scan-composite-action-ast.js +9 -11
  21. package/dist/core/ast/scanners/scan-workflow-ast.js +12 -14
  22. package/dist/core/ast/update/apply-updates.js +24 -27
  23. package/dist/core/ast/utils/extract-uses-from-steps.js +12 -14
  24. package/dist/core/ast/utils/find-map-pair.js +2 -5
  25. package/dist/core/ast/utils/get-line-number.js +4 -4
  26. package/dist/core/constants.js +1 -3
  27. package/dist/core/filters/parse-exclude-patterns.d.ts +17 -0
  28. package/dist/core/filters/parse-exclude-patterns.js +23 -0
  29. package/dist/core/fs/is-yaml-file.js +2 -2
  30. package/dist/core/fs/read-yaml-document.js +4 -5
  31. package/dist/core/ignore/should-ignore.d.ts +23 -0
  32. package/dist/core/ignore/should-ignore.js +14 -0
  33. package/dist/core/interactive/format-version.js +16 -30
  34. package/dist/core/interactive/pad-string.js +5 -6
  35. package/dist/core/interactive/prompt-update-selection.js +106 -163
  36. package/dist/core/interactive/strip-ansi.js +10 -17
  37. package/dist/core/parsing/parse-action-reference.js +23 -23
  38. package/dist/core/scan-action-file.js +3 -3
  39. package/dist/core/scan-github-actions.js +87 -136
  40. package/dist/core/scan-workflow-file.js +3 -3
  41. package/dist/core/schema/composite/is-composite-action-runs.js +2 -4
  42. package/dist/core/schema/composite/is-composite-action-structure.js +4 -4
  43. package/dist/core/schema/workflow/is-workflow-structure.js +4 -4
  44. package/dist/package.js +1 -1
  45. package/package.json +1 -1
  46. package/readme.md +55 -8
@@ -1,34 +1,34 @@
1
- function parseActionReference(reference, file, line) {
2
- if (!reference || reference.trim() === "") return null;
3
- if (reference.startsWith("docker://")) return {
1
+ function parseActionReference(e, t, n) {
2
+ if (!e || e.trim() === "") return null;
3
+ if (e.startsWith("docker://")) return {
4
4
  version: void 0,
5
- name: reference,
5
+ name: e,
6
6
  type: "docker",
7
- file,
8
- line
7
+ file: t,
8
+ line: n
9
9
  };
10
- if (reference.startsWith("./") || reference.startsWith("../")) return {
10
+ if (e.startsWith("./") || e.startsWith("../")) return {
11
11
  version: void 0,
12
- name: reference,
12
+ name: e,
13
13
  type: "local",
14
- file,
15
- line
14
+ file: t,
15
+ line: n
16
16
  };
17
- let parts = reference.split("@");
18
- if (parts.length !== 2) return null;
19
- let [namePart, version] = parts;
20
- if (!namePart || !version) return null;
21
- let segs = namePart.split("/");
22
- if (segs.length < 2) return null;
23
- let [owner, repo] = segs;
24
- if (!owner || !repo) return null;
25
- for (let seg of segs.slice(2)) if (!seg) return null;
17
+ let r = e.split("@");
18
+ if (r.length !== 2) return null;
19
+ let [i, a] = r;
20
+ if (!i || !a) return null;
21
+ let o = i.split("/");
22
+ if (o.length < 2) return null;
23
+ let [s, c] = o;
24
+ if (!s || !c) return null;
25
+ for (let e of o.slice(2)) if (!e) return null;
26
26
  return {
27
27
  type: "external",
28
- name: namePart,
29
- version,
30
- file,
31
- line
28
+ name: i,
29
+ version: a,
30
+ file: t,
31
+ line: n
32
32
  };
33
33
  }
34
34
  export { parseActionReference };
@@ -1,7 +1,7 @@
1
1
  import { readYamlDocument } from "./fs/read-yaml-document.js";
2
2
  import { scanCompositeActionAst } from "./ast/scanners/scan-composite-action-ast.js";
3
- async function scanActionFile(filePath) {
4
- let { document, content } = await readYamlDocument(filePath);
5
- return scanCompositeActionAst(document, content, filePath);
3
+ async function scanActionFile(n) {
4
+ let { document: r, content: i } = await readYamlDocument(n);
5
+ return scanCompositeActionAst(r, i, n);
6
6
  }
7
7
  export { scanActionFile };
@@ -4,190 +4,141 @@ import { scanActionFile } from "./scan-action-file.js";
4
4
  import { isYamlFile } from "./fs/is-yaml-file.js";
5
5
  import { readFile, readdir, stat } from "node:fs/promises";
6
6
  import { isAbsolute, join, relative, resolve } from "node:path";
7
- async function scanGitHubActions(rootPath = process.cwd()) {
8
- let result = {
7
+ async function scanGitHubActions(d = process.cwd()) {
8
+ let m = {
9
9
  compositeActions: /* @__PURE__ */ new Map(),
10
10
  workflows: /* @__PURE__ */ new Map(),
11
11
  actions: []
12
- };
13
- let normalizedRoot = resolve(rootPath);
14
- function isWithin(root, candidate) {
15
- let relativePath = relative(root, candidate);
16
- return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
12
+ }, h = resolve(d);
13
+ function g(e, o) {
14
+ let s = relative(e, o);
15
+ return s !== "" && !s.startsWith("..") && !isAbsolute(s);
17
16
  }
18
- let githubPath = join(normalizedRoot, GITHUB_DIRECTORY);
19
- if (!isWithin(normalizedRoot, githubPath)) throw new Error("Invalid path: detected path traversal attempt");
20
- function isValidName(name) {
21
- if (name.includes("..") || name.includes("/") || name.includes("\\")) {
22
- console.warn(`Skipping invalid name: ${name}`);
23
- return false;
24
- }
25
- return true;
17
+ let _ = join(h, GITHUB_DIRECTORY);
18
+ if (!g(h, _)) throw Error("Invalid path: detected path traversal attempt");
19
+ function v(e) {
20
+ return e.includes("..") || e.includes("/") || e.includes("\\") ? (console.warn(`Skipping invalid name: ${e}`), !1) : !0;
26
21
  }
27
- let workflowsPath = join(githubPath, WORKFLOWS_DIRECTORY);
28
- if (!isWithin(normalizedRoot, workflowsPath)) return result;
22
+ let y = join(_, WORKFLOWS_DIRECTORY);
23
+ if (!g(h, y)) return m;
29
24
  try {
30
- let workflowsStat = await stat(workflowsPath);
31
- if (workflowsStat.isDirectory()) {
32
- let files = await readdir(workflowsPath);
33
- let workflowPromises = files.filter((file) => {
34
- if (!isValidName(file)) return false;
35
- return isYamlFile(file);
36
- }).map(async (file) => {
37
- let filePath = join(workflowsPath, file);
38
- if (!isWithin(workflowsPath, filePath)) {
39
- console.warn(`Skipping file outside workflows directory: ${file}`);
40
- return {
41
- success: false,
42
- actions: [],
43
- path: ""
44
- };
45
- }
25
+ if ((await stat(y)).isDirectory()) {
26
+ let e = (await readdir(y)).filter((e) => v(e) ? isYamlFile(e) : !1).map(async (e) => {
27
+ let l = join(y, e);
28
+ if (!g(y, l)) return console.warn(`Skipping file outside workflows directory: ${e}`), {
29
+ success: !1,
30
+ actions: [],
31
+ path: ""
32
+ };
46
33
  try {
47
- let actions = await scanWorkflowFile(filePath);
34
+ let u = await scanWorkflowFile(l);
48
35
  return {
49
- path: `${GITHUB_DIRECTORY}/${WORKFLOWS_DIRECTORY}/${file}`,
50
- success: true,
51
- actions
36
+ path: `${GITHUB_DIRECTORY}/${WORKFLOWS_DIRECTORY}/${e}`,
37
+ success: !0,
38
+ actions: u
52
39
  };
53
40
  } catch {
54
41
  return {
55
- path: `${GITHUB_DIRECTORY}/${WORKFLOWS_DIRECTORY}/${file}`,
56
- success: false,
42
+ path: `${GITHUB_DIRECTORY}/${WORKFLOWS_DIRECTORY}/${e}`,
43
+ success: !1,
57
44
  actions: []
58
45
  };
59
46
  }
60
- });
61
- let workflowResults = await Promise.all(workflowPromises);
62
- for (let workflow of workflowResults) if (workflow.success && workflow.path) if (workflow.actions.length > 0) {
63
- result.workflows.set(workflow.path, workflow.actions);
64
- result.actions.push(...workflow.actions);
65
- } else result.workflows.set(workflow.path, []);
47
+ }), l = await Promise.all(e);
48
+ for (let e of l) e.success && e.path && (e.actions.length > 0 ? (m.workflows.set(e.path, e.actions), m.actions.push(...e.actions)) : m.workflows.set(e.path, []));
66
49
  }
67
50
  } catch {}
68
- let actionsPath = join(githubPath, ACTIONS_DIRECTORY);
69
- if (!isWithin(normalizedRoot, actionsPath)) return result;
51
+ let b = join(_, ACTIONS_DIRECTORY);
52
+ if (!g(h, b)) return m;
70
53
  try {
71
- let actionsStat = await stat(actionsPath);
72
- if (actionsStat.isDirectory()) {
73
- let subdirectories = await readdir(actionsPath);
74
- let actionPromises = subdirectories.map(async (subdir) => {
75
- if (!isValidName(subdir)) return null;
76
- let subdirPath = join(actionsPath, subdir);
77
- if (!isWithin(actionsPath, subdirPath)) {
78
- console.warn(`Skipping subdirectory outside actions path: ${subdir}`);
79
- return null;
80
- }
54
+ if ((await stat(b)).isDirectory()) {
55
+ let s = (await readdir(b)).map(async (s) => {
56
+ if (!v(s)) return null;
57
+ let c = join(b, s);
58
+ if (!g(b, c)) return console.warn(`Skipping subdirectory outside actions path: ${s}`), null;
81
59
  try {
82
- let subdirectoryStat = await stat(subdirPath);
83
- if (!subdirectoryStat.isDirectory()) return null;
84
- let actionFilePath = join(subdirPath, "action.yml");
85
- if (!isWithin(subdirPath, actionFilePath)) return null;
86
- let actions = [];
60
+ if (!(await stat(c)).isDirectory()) return null;
61
+ let u = join(c, "action.yml");
62
+ if (!g(c, u)) return null;
63
+ let d = [];
87
64
  try {
88
- actions = await scanActionFile(actionFilePath);
65
+ d = await scanActionFile(u);
89
66
  } catch {
90
67
  try {
91
- actionFilePath = join(subdirPath, "action.yaml");
92
- if (!isWithin(subdirPath, actionFilePath)) return null;
93
- actions = await scanActionFile(actionFilePath);
68
+ if (u = join(c, "action.yaml"), !g(c, u)) return null;
69
+ d = await scanActionFile(u);
94
70
  } catch {
95
71
  return null;
96
72
  }
97
73
  }
98
74
  return {
99
- path: `${GITHUB_DIRECTORY}/${ACTIONS_DIRECTORY}/${subdir}`,
100
- name: subdir,
101
- actions
75
+ path: `${GITHUB_DIRECTORY}/${ACTIONS_DIRECTORY}/${s}`,
76
+ name: s,
77
+ actions: d
102
78
  };
103
79
  } catch {
104
80
  return null;
105
81
  }
106
- });
107
- let actionResults = await Promise.all(actionPromises);
108
- for (let actionResult of actionResults) if (actionResult) {
109
- result.compositeActions.set(actionResult.name, actionResult.path);
110
- result.actions.push(...actionResult.actions);
111
- }
82
+ }), c = await Promise.all(s);
83
+ for (let e of c) e && (m.compositeActions.set(e.name, e.path), m.actions.push(...e.actions));
112
84
  }
113
85
  } catch {}
114
86
  try {
115
- let repoSlug = await getCurrentRepoSlug(normalizedRoot);
116
- if (repoSlug) {
117
- if (process.env["ACTIONS_UP_TEST_THROW"] === "1") throw new Error("test");
118
- let seenCompositeDirectories = /* @__PURE__ */ new Set();
119
- let queue = [];
120
- for (let action of result.actions) {
121
- if (action.type !== "external") continue;
122
- let segs = action.name.split("/");
123
- if (segs.length < 3) continue;
124
- let candidateSlug = `${segs[0]}/${segs[1]}`;
125
- if (candidateSlug !== repoSlug) continue;
126
- let compositeDirectory = join(normalizedRoot, ...segs.slice(2));
127
- if (!isWithin(normalizedRoot, compositeDirectory)) continue;
128
- if (seenCompositeDirectories.has(compositeDirectory)) continue;
129
- seenCompositeDirectories.add(compositeDirectory);
130
- queue.push(compositeDirectory);
87
+ let e = await getCurrentRepoSlug(h);
88
+ if (e) {
89
+ if (process.env.ACTIONS_UP_TEST_THROW === "1") throw Error("test");
90
+ let o = /* @__PURE__ */ new Set(), s = [];
91
+ for (let c of m.actions) {
92
+ if (c.type !== "external") continue;
93
+ let l = c.name.split("/");
94
+ if (l.length < 3 || `${l[0]}/${l[1]}` !== e) continue;
95
+ let u = join(h, ...l.slice(2));
96
+ if (!g(h, u) || o.has(u)) continue;
97
+ o.add(u), s.push(u);
131
98
  }
132
- async function processQueue() {
133
- if (queue.length === 0) return;
134
- let batch = queue.splice(0);
135
- let discoveredNext = await Promise.all(batch.map(async (directory) => {
99
+ async function c() {
100
+ if (s.length === 0) return;
101
+ let u = s.splice(0), d = await Promise.all(u.map(async (s) => {
136
102
  try {
137
- let ymlPath = join(directory, "action.yml");
138
- let yamlPath = join(directory, "action.yaml");
139
- let filePath = ymlPath;
103
+ let c = join(s, "action.yml"), u = join(s, "action.yaml"), d = c;
140
104
  try {
141
- let fileInfo = await stat(ymlPath);
142
- if (!fileInfo.isFile()) throw new Error("not a file");
105
+ if (!(await stat(c)).isFile()) throw Error("not a file");
143
106
  } catch {
144
- let yamlInfo = await stat(yamlPath);
145
- if (!yamlInfo.isFile()) throw new Error("not a file");
146
- filePath = yamlPath;
107
+ if (!(await stat(u)).isFile()) throw Error("not a file");
108
+ d = u;
147
109
  }
148
- let nestedActions = await scanActionFile(filePath);
149
- if (nestedActions.length > 0) result.actions.push(...nestedActions);
150
- let nextDirectories = [];
151
- for (let nestedAction of nestedActions) {
152
- if (nestedAction.type !== "external") continue;
153
- let nameSegments = nestedAction.name.split("/");
154
- if (nameSegments.length < 3) continue;
155
- let nameSlug = `${nameSegments[0]}/${nameSegments[1]}`;
156
- if (nameSlug !== repoSlug) continue;
157
- let nextDirectory = join(normalizedRoot, ...nameSegments.slice(2));
158
- if (!isWithin(normalizedRoot, nextDirectory)) continue;
159
- if (seenCompositeDirectories.has(nextDirectory)) continue;
160
- seenCompositeDirectories.add(nextDirectory);
161
- nextDirectories.push(nextDirectory);
110
+ let f = await scanActionFile(d);
111
+ f.length > 0 && m.actions.push(...f);
112
+ let p = [];
113
+ for (let s of f) {
114
+ if (s.type !== "external") continue;
115
+ let c = s.name.split("/");
116
+ if (c.length < 3 || `${c[0]}/${c[1]}` !== e) continue;
117
+ let l = join(h, ...c.slice(2));
118
+ if (!g(h, l) || o.has(l)) continue;
119
+ o.add(l), p.push(l);
162
120
  }
163
- return nextDirectories;
121
+ return p;
164
122
  } catch {
165
123
  return [];
166
124
  }
167
125
  }));
168
- for (let list of discoveredNext) for (let directory of list) queue.push(directory);
169
- await processQueue();
126
+ for (let e of d) for (let o of e) s.push(o);
127
+ await c();
170
128
  }
171
- await processQueue();
129
+ await c();
172
130
  }
173
131
  } catch {}
174
- return result;
132
+ return m;
175
133
  }
176
- async function getCurrentRepoSlug(root) {
177
- let environmentSlug = process.env["GITHUB_REPOSITORY"];
178
- if (environmentSlug && /^[^\s/]+\/[^\s/]+$/u.test(environmentSlug)) return environmentSlug;
134
+ async function getCurrentRepoSlug(e) {
135
+ let o = process.env.GITHUB_REPOSITORY;
136
+ if (o && /^[^\s/]+\/[^\s/]+$/u.test(o)) return o;
179
137
  try {
180
- let gitConfigPath = join(root, ".git", "config");
181
- let content = await readFile(gitConfigPath, "utf8");
182
- let originUrlMatch = content.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(?<url>.+)/u);
183
- let url = originUrlMatch?.groups?.["url"]?.trim();
184
- if (!url) {
185
- let anyUrlMatch = content.match(/url\s*=\s*(?<url>.+)/u);
186
- url = anyUrlMatch?.groups?.["url"]?.trim();
187
- }
188
- if (!url) return null;
189
- let httpsMatch = url.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^./]+)(?:\.git)?$/u);
190
- if (httpsMatch?.groups) return `${httpsMatch.groups["owner"]}/${httpsMatch.groups["repo"]}`;
138
+ let o = join(e, ".git", "config"), s = await readFile(o, "utf8"), c = s.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim();
139
+ if (c ||= s.match(/url\s*=\s*(?<url>.+)/u)?.groups?.url?.trim(), !c) return null;
140
+ let l = c.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^./]+)(?:\.git)?$/u);
141
+ if (l?.groups) return `${l.groups.owner}/${l.groups.repo}`;
191
142
  } catch {}
192
143
  return null;
193
144
  }
@@ -1,7 +1,7 @@
1
1
  import { scanWorkflowAst } from "./ast/scanners/scan-workflow-ast.js";
2
2
  import { readYamlDocument } from "./fs/read-yaml-document.js";
3
- async function scanWorkflowFile(filePath) {
4
- let { document, content } = await readYamlDocument(filePath);
5
- return scanWorkflowAst(document, content, filePath);
3
+ async function scanWorkflowFile(n) {
4
+ let { document: r, content: i } = await readYamlDocument(n);
5
+ return scanWorkflowAst(r, i, n);
6
6
  }
7
7
  export { scanWorkflowFile };
@@ -1,6 +1,4 @@
1
- function isCompositeActionRuns(value) {
2
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3
- let object = value;
4
- return "using" in object;
1
+ function isCompositeActionRuns(e) {
2
+ return typeof e != "object" || !e || Array.isArray(e) ? !1 : "using" in e;
5
3
  }
6
4
  export { isCompositeActionRuns };
@@ -1,6 +1,6 @@
1
- function isCompositeActionStructure(value) {
2
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3
- let object = value;
4
- return "name" in object || "description" in object || "runs" in object;
1
+ function isCompositeActionStructure(e) {
2
+ if (typeof e != "object" || !e || Array.isArray(e)) return !1;
3
+ let t = e;
4
+ return "name" in t || "description" in t || "runs" in t;
5
5
  }
6
6
  export { isCompositeActionStructure };
@@ -1,6 +1,6 @@
1
- function isWorkflowStructure(value) {
2
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3
- let object = value;
4
- return "on" in object || "name" in object || "jobs" in object;
1
+ function isWorkflowStructure(e) {
2
+ if (typeof e != "object" || !e || Array.isArray(e)) return !1;
3
+ let t = e;
4
+ return "on" in t || "name" in t || "jobs" in t;
5
5
  }
6
6
  export { isWorkflowStructure };
package/dist/package.js CHANGED
@@ -1,2 +1,2 @@
1
- const version = "1.3.1";
1
+ const version = "1.4.1";
2
2
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actions-up",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  <img
4
4
  src="https://raw.githubusercontent.com/azat-io/actions-up/main/assets/logo.svg"
5
- alt="Actions Up! logo"
5
+ alt="Actions Up logo"
6
6
  width="160"
7
7
  height="160"
8
8
  align="right"
@@ -19,7 +19,7 @@ Interactively upgrade and pin actions to exact commit SHAs for secure, reproduci
19
19
  ## Features
20
20
 
21
21
  - **Auto-discovery**: Scans all workflows (`.github/workflows/*.yml`) and composite actions (`.github/actions/*/action.yml`)
22
- - **SHA Pinning**: Updates actions to use commit SHA instead of tags for better security
22
+ - **SHA pinning**: Updates actions to use commit SHA instead of tags for better security
23
23
  - **Batch Updates**: Update multiple actions at once
24
24
  - **Interactive Selection**: Choose which actions to update
25
25
  - **Breaking Changes Detection**: Warns about major version updates
@@ -41,7 +41,7 @@ Interactively upgrade and pin actions to exact commit SHAs for secure, reproduci
41
41
  />
42
42
  <img
43
43
  src="https://raw.githubusercontent.com/azat-io/actions-up/main/assets/example-light.webp"
44
- alt="Actions Up interactive example"
44
+ alt="Actions Up! interactive example"
45
45
  width="820"
46
46
  />
47
47
  </picture>
@@ -209,7 +209,7 @@ jobs:
209
209
  echo "#### Option 2: Manual Update"
210
210
  echo "1. Review each update in the table above"
211
211
  echo "2. For breaking changes, click the Release Notes link to review changes"
212
- echo "3. Edit the workflow files and update the version numbers"
212
+ echo "3. Edit the workflows and update the version numbers"
213
213
  echo "4. Test the changes in your CI/CD pipeline"
214
214
  echo ""
215
215
  echo "---"
@@ -233,7 +233,7 @@ jobs:
233
233
  echo ""
234
234
  echo "### All GitHub Actions in this repository are up to date!"
235
235
  echo ""
236
- echo "No action required. Your workflow files are using the latest versions of all GitHub Actions."
236
+ echo "No action required. Your workflows are using the latest versions of all GitHub Actions."
237
237
  } > actions-up-report.md
238
238
 
239
239
  echo "has-updates=false" >> $GITHUB_OUTPUT
@@ -335,7 +335,7 @@ jobs:
335
335
  echo "::error:: Found ${{ steps.actions-check.outputs.update-count }} outdated GitHub Actions. Please update them before merging."
336
336
  echo ""
337
337
  echo "You can update them by running: npx actions-up"
338
- echo "Or manually update the versions in your workflow files."
338
+ echo "Or manually update the versions in your workflows."
339
339
  exit 1
340
340
  ````
341
341
 
@@ -408,12 +408,59 @@ Or in GitHub Actions:
408
408
  run: npx actions-up --dry-run
409
409
  ```
410
410
 
411
+ ### Skipping Updates
412
+
413
+ Skip updates using CLI excludes and YAML ignore comments. Excludes run first, then ignore comments.
414
+
415
+ #### CLI Excludes
416
+
417
+ Skip actions by name using regular expressions. Patterns are matched against the full action name (`owner/repo[/path]`).
418
+
419
+ - Repeatable flag: `--exclude <regex>` (can be used multiple times)
420
+ - Comma-separated list is supported inside a single flag
421
+ - Forms:
422
+ - Plain string compiled as case-insensitive regex: `my-org/.*`
423
+ - Literal with flags: `/^actions\/internal-.+$/i`
424
+
425
+ Examples:
426
+
427
+ ```bash
428
+ npx actions-up --exclude "my-org/.*"
429
+ npx actions-up --exclude ".*/internal-.*" --exclude "/^acme\/.+$/i"
430
+ # or
431
+ npx actions-up --exclude "my-org/.*, .*/internal-.*"
432
+ ```
433
+
434
+ #### Ignore Comments
435
+
436
+ You can skip specific actions or files using YAML comments. Ignored items are hidden in dry-run and interactive modes and are not updated with `--yes`.
437
+
438
+ - Ignore whole file: `# actions-up-ignore-file`
439
+ - Block ignore: `# actions-up-ignore-start` … `# actions-up-ignore-end`
440
+ - Next line: `# actions-up-ignore-next-line`
441
+ - Inline on the same line: append `# actions-up-ignore`
442
+
443
+ Example:
444
+
445
+ ```yaml
446
+ # actions-up-ignore-file
447
+
448
+ # actions-up-ignore-next-line
449
+ - uses: actions/checkout@v3
450
+
451
+ - uses: actions/setup-node@v3 # actions-up-ignore
452
+
453
+ # actions-up-ignore-start
454
+ - uses: actions/cache@v3
455
+ # actions-up-ignore-end
456
+ ```
457
+
411
458
  ## Security
412
459
 
413
460
  Actions Up promotes security best practices:
414
461
 
415
- - **SHA Pinning**: Uses commit SHA instead of mutable tags
416
- - **Version Comments**: Adds version as comment for readability
462
+ - **SHA pinning**: Uses commit SHA instead of mutable tags
463
+ - **Version comment**: Adds the released version next to the pinned SHA for readability
417
464
  - **No Auto-Updates**: Full control over what gets updated
418
465
  - **Breaking Change Warnings**: Alerts you to major version updates that may require configuration changes
419
466